From bc31dedf5b27134d131d965f5381d5194a272511 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 08:26:04 +0100 Subject: [PATCH 01/12] feat(status): implement status page for app startup - Add StatusPage widget to display status during critical startup sequence - Handle config fetching and error states - Implement retry functionality for failed config fetch - Update view exports to include new StatusPage --- lib/status/view/status_page.dart | 53 ++++++++++++++++++++++++++++++++ lib/status/view/view.dart | 1 + 2 files changed, 54 insertions(+) create mode 100644 lib/status/view/status_page.dart diff --git a/lib/status/view/status_page.dart b/lib/status/view/status_page.dart new file mode 100644 index 0000000..e477245 --- /dev/null +++ b/lib/status/view/status_page.dart @@ -0,0 +1,53 @@ +import 'package:core/core.dart' hide AppStatus; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// A page that serves as the root UI during the critical startup sequence. +/// +/// This widget is displayed *before* the main application's router and UI +/// shell are built. It is responsible for showing the user a clear status +/// while the remote configuration is being fetched, and it provides a way +/// for the user to retry if the fetch operation fails. +class StatusPage extends StatelessWidget { + /// {@macro status_page} + const StatusPage({super.key}); + + @override + Widget build(BuildContext context) { + // This page is a temporary root widget shown during the critical + // startup phase before the main app UI (and GoRouter) is built. + // It handles two key states: fetching the remote configuration and + // recovering from a failed fetch. + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final l10n = AppLocalizationsX(context).l10n; + + if (state.status == AppStatus.configFetching) { + // While fetching configuration, display a clear loading indicator. + // This uses a shared widget from the UI kit for consistency. + return LoadingStateWidget( + icon: Icons.settings_applications_outlined, + headline: l10n.headlinesFeedLoadingHeadline, + subheadline: l10n.pleaseWait, + ); + } + + // If fetching fails, show an error message with a retry option. + // This allows the user to recover from transient network issues. + return FailureStateWidget( + exception: const NetworkException(), // A generic network error + retryButtonText: 'l10n.retryButtonText', //TODO(fulleni): localize me. + onRetry: () { + // Dispatch the event to AppBloc to re-trigger the fetch. + context.read().add(const AppConfigFetchRequested()); + }, + ); + }, + ), + ); + } +} diff --git a/lib/status/view/view.dart b/lib/status/view/view.dart index 7c24eda..9ef4196 100644 --- a/lib/status/view/view.dart +++ b/lib/status/view/view.dart @@ -1,2 +1,3 @@ export 'maintenance_page.dart'; +export 'status_page.dart'; export 'update_required_page.dart'; From 236768b80906ab7dc29688b41de03f807a5286c9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 08:31:07 +0100 Subject: [PATCH 02/12] refactor(app): integrate critical sequence checks into AppBloc - Move maintenance and update checks from AppOpened event into _fetchAppConfig - Remove AppOpened event handler - Ensure remote config is fetched before evaluating app status - Implement a prioritized check sequence: maintenance > update > user role --- lib/app/bloc/app_bloc.dart | 64 ++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 52bc974..2de32ef 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -59,7 +59,6 @@ class AppBloc extends Bloc { on(_onFontFamilyChanged); on(_onAppTextScaleFactorChanged); on(_onAppFontWeightChanged); - on(_onAppOpened); // Listen directly to the auth state changes stream _userSubscription = _authenticationRepository.authStateChanges.listen( @@ -438,18 +437,41 @@ class AppBloc extends Bloc { '[AppBloc] Remote Config fetched successfully. ID: ${remoteConfig.id} for user: ${state.user!.id}', ); - // Determine the correct status based on the existing user's role. - // This ensures that successfully fetching config doesn't revert auth status to 'initial'. - final newStatusBasedOnUser = - state.user!.appRole == AppUserRole.standardUser + // --- CRITICAL STARTUP SEQUENCE EVALUATION --- + // After successfully fetching the remote configuration, we must + // evaluate the app's status in a specific order before allowing + // the main UI to be built. + + // 1. Check for Maintenance Mode. This has the highest priority. + if (remoteConfig.appStatus.isUnderMaintenance) { + emit( + state.copyWith( + status: AppStatus.underMaintenance, + remoteConfig: remoteConfig, + ), + ); + return; + } + + // 2. Check for a Required Update. + // TODO(fulleni): Compare with actual app version from package_info_plus. + if (remoteConfig.appStatus.isLatestVersionOnly) { + emit( + state.copyWith( + status: AppStatus.updateRequired, + remoteConfig: remoteConfig, + ), + ); + return; + } + + // 3. If no critical status is active, proceed to the normal app state. + // The status is determined by the user's role (authenticated/anonymous). + final finalStatus = state.user!.appRole == AppUserRole.standardUser ? AppStatus.authenticated : AppStatus.anonymous; - emit( - state.copyWith( - remoteConfig: remoteConfig, - status: newStatusBasedOnUser, - ), - ); + + emit(state.copyWith(remoteConfig: remoteConfig, status: finalStatus)); } on HttpException catch (e) { print( '[AppBloc] Failed to fetch AppConfig (HttpException) for user ${state.user?.id}: ${e.runtimeType} - ${e.message}', @@ -524,24 +546,4 @@ class AppBloc extends Bloc { ); } } - - Future _onAppOpened(AppOpened event, Emitter emit) async { - if (state.remoteConfig == null) { - return; - } - - final appStatus = state.remoteConfig!.appStatus; - - if (appStatus.isUnderMaintenance) { - emit(state.copyWith(status: AppStatus.underMaintenance)); - return; - } - - // TODO(fulleni): Get the current app version from a package like - // package_info_plus and compare it with appStatus.latestAppVersion. - if (appStatus.isLatestVersionOnly) { - emit(state.copyWith(status: AppStatus.updateRequired)); - return; - } - } } From 84863548bb48b9c6f3da8f919cc55e198efbe7d4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 08:41:45 +0100 Subject: [PATCH 03/12] refactor(app): implement stable startup with full-screen status pages - Introduce a new stable startup architecture using BlocBuilder - Add full-screen status pages for maintenance, update required, and config fetching states - Move main app UI build to a more stable point in the app lifecycle - Simplify theme and localization setup in MaterialApp - Update StatusPage to use new localization approach --- lib/app/view/app.dart | 150 +++++++++---------------------- lib/status/view/status_page.dart | 6 +- 2 files changed, 46 insertions(+), 110 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 3f7c98e..7ae6d91 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -9,7 +9,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_e import 'package:flutter_news_app_mobile_client_full_source_code/app/services/app_status_service.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_migration_service.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart'; -import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/router/router.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart'; import 'package:go_router/go_router.dart'; @@ -191,132 +191,68 @@ class _AppViewState extends State<_AppView> { listener: (context, state) { _statusNotifier.value = state.status; }, + // The BlocBuilder is the core of the new stable startup architecture. + // It acts as a high-level switch that determines which UI to show based + // on the application's status. child: BlocBuilder( - // Rebuild the UI based on AppBloc's state (theme, locale, and critical app statuses) builder: (context, state) { - // Defer l10n access until inside a MaterialApp context + // --- Full-Screen Status Pages --- + // The following states represent critical, app-wide conditions that + // must be handled before the main router and UI are displayed. + // By returning a dedicated widget here, we ensure these pages are + // full-screen and exist outside the main app's navigation shell. - // Handle critical RemoteConfig loading states globally - // These checks have the highest priority and will lock the entire UI. - // - // Check for Maintenance Mode. if (state.status == AppStatus.underMaintenance) { - return const MaterialApp( + // The app is in maintenance mode. Show the MaintenancePage. + // It's wrapped in a basic MaterialApp to provide theme and l10n. + return MaterialApp( debugShowCheckedModeBanner: false, - home: MaintenancePage(), + theme: lightTheme(), + darkTheme: darkTheme(), + themeMode: state.themeMode, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const MaintenancePage(), ); } - // Check for a Required Update. if (state.status == AppStatus.updateRequired) { - return const MaterialApp( - debugShowCheckedModeBanner: false, - home: UpdateRequiredPage(), - ); - } - - // Check for Config Fetching state. - if (state.status == AppStatus.configFetching) { + // A mandatory update is required. Show the UpdateRequiredPage. return MaterialApp( debugShowCheckedModeBanner: false, - theme: lightTheme( - scheme: FlexScheme.material, - appTextScaleFactor: AppTextScaleFactor.medium, - appFontWeight: AppFontWeight.regular, - fontFamily: null, - ), - darkTheme: darkTheme( - scheme: FlexScheme.material, - appTextScaleFactor: AppTextScaleFactor.medium, - appFontWeight: AppFontWeight.regular, - fontFamily: null, // System default font - ), - themeMode: state - .themeMode, // Still respect light/dark if available from system - localizationsDelegates: const [ - ...AppLocalizations.localizationsDelegates, - ...UiKitLocalizations.localizationsDelegates, - ], - supportedLocales: const [ - ...AppLocalizations.supportedLocales, - ...UiKitLocalizations.supportedLocales, - ], - home: Scaffold( - body: Builder( - // Use Builder to get context under MaterialApp - builder: (innerContext) { - return LoadingStateWidget( - icon: Icons.settings_applications_outlined, - headline: AppLocalizations.of( - innerContext, - ).headlinesFeedLoadingHeadline, - subheadline: AppLocalizations.of(innerContext).pleaseWait, - ); - }, - ), - ), + theme: lightTheme(), + darkTheme: darkTheme(), + themeMode: state.themeMode, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const UpdateRequiredPage(), ); } - if (state.status == AppStatus.configFetchFailed) { + if (state.status == AppStatus.configFetching || + state.status == AppStatus.configFetchFailed) { + // The app is in the process of fetching its initial remote + // configuration or has failed to do so. The StatusPage handles + // both the loading indicator and the retry mechanism. return MaterialApp( debugShowCheckedModeBanner: false, - theme: lightTheme( - scheme: FlexScheme.material, - appTextScaleFactor: AppTextScaleFactor.medium, - appFontWeight: AppFontWeight.regular, - fontFamily: null, - ), - darkTheme: darkTheme( - scheme: FlexScheme.material, - appTextScaleFactor: AppTextScaleFactor.medium, - appFontWeight: AppFontWeight.regular, - fontFamily: null, - ), + theme: lightTheme(), + darkTheme: darkTheme(), themeMode: state.themeMode, - localizationsDelegates: const [ - ...AppLocalizations.localizationsDelegates, - ...UiKitLocalizations.localizationsDelegates, - ], - supportedLocales: const [ - ...AppLocalizations.supportedLocales, - ...UiKitLocalizations.supportedLocales, - ], - home: Scaffold( - body: Builder( - // Use Builder to get context under MaterialApp - builder: (innerContext) { - return FailureStateWidget( - exception: const NetworkException(), - retryButtonText: UiKitLocalizations.of( - innerContext, - )!.retryButtonText, - onRetry: () { - // Use outer context for BLoC access - context.read().add( - const AppConfigFetchRequested(), - ); - }, - ); - }, - ), - ), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const StatusPage(), ); } - // If config is loaded (or not in a failed/fetching state for config), proceed with main app UI - // It's safe to access l10n here if needed for print statements, - // as this path implies we are about to build the main MaterialApp.router - // which provides localizations. - // final l10n = context.l10n; - print('[_AppViewState] Building MaterialApp.router'); - print('[_AppViewState] state.fontFamily: ${state.fontFamily}'); - print( - '[_AppViewState] state.settings.displaySettings.fontFamily: ${state.settings.displaySettings.fontFamily}', - ); - print( - '[_AppViewState] state.settings.displaySettings.fontWeight: ${state.settings.displaySettings.fontWeight}', - ); + // --- Main Application UI --- + // If none of the critical states above are met, the app is ready + // to display its main UI. We build the MaterialApp.router here. + // + // This is the STABLE root of the main application. Because it is + // only built when the app is in a "running" state, it will not be + // destroyed and rebuilt during startup, which fixes the + // `BuildContext` instability and related crashes. return MaterialApp.router( debugShowCheckedModeBanner: false, themeMode: state.themeMode, diff --git a/lib/status/view/status_page.dart b/lib/status/view/status_page.dart index e477245..b4a8752 100644 --- a/lib/status/view/status_page.dart +++ b/lib/status/view/status_page.dart @@ -1,4 +1,4 @@ -import 'package:core/core.dart' hide AppStatus; +import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; @@ -24,7 +24,7 @@ class StatusPage extends StatelessWidget { return Scaffold( body: BlocBuilder( builder: (context, state) { - final l10n = AppLocalizationsX(context).l10n; + final l10n = context.l10n; if (state.status == AppStatus.configFetching) { // While fetching configuration, display a clear loading indicator. @@ -40,7 +40,7 @@ class StatusPage extends StatelessWidget { // This allows the user to recover from transient network issues. return FailureStateWidget( exception: const NetworkException(), // A generic network error - retryButtonText: 'l10n.retryButtonText', //TODO(fulleni): localize me. + retryButtonText: l10n.retryButtonText, onRetry: () { // Dispatch the event to AppBloc to re-trigger the fetch. context.read().add(const AppConfigFetchRequested()); From e4e390c88fcb16f9863bd2b98e956db91518138f Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 08:43:50 +0100 Subject: [PATCH 04/12] refactor(router): simplify redirect logic in GoRouter - Remove unnecessary checks for app config and URI - Consolidate print statements and remove detailed debug logs - Simplify unauthenticated user redirect logic - Reduce complexity in anonymous/authenticated user handling - Remove fallback case, as all statuses should be handled --- lib/router/router.dart | 119 +++++++++++------------------------------ 1 file changed, 31 insertions(+), 88 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index bcbd0bc..ecb110a 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -78,16 +78,12 @@ GoRouter createRouter({ // --- Redirect Logic --- redirect: (BuildContext context, GoRouterState state) { final appStatus = context.read().state.status; - final appConfig = context.read().state.remoteConfig; final currentLocation = state.matchedLocation; - final currentUri = state.uri; print( 'GoRouter Redirect Check:\n' ' Current Location (Matched): $currentLocation\n' - ' Current URI (Full): $currentUri\n' - ' AppStatus: $appStatus\n' - ' AppConfig isNull: ${appConfig == null}', + ' AppStatus: $appStatus', ); // --- Define Key Paths --- @@ -95,108 +91,55 @@ GoRouter createRouter({ const feedPath = Routes.feed; final isGoingToAuth = currentLocation.startsWith(authenticationPath); - // --- Case 0: App is Initializing or Config is being fetched/failed --- - if (appStatus == AppStatus.initial || - appStatus == AppStatus.configFetching || - appStatus == AppStatus.configFetchFailed) { - // If AppStatus is initial and trying to go to a non-auth page (e.g. initial /feed) - // redirect to auth immediately to settle auth status first. - if (appStatus == AppStatus.initial && !isGoingToAuth) { - print( - ' Redirect Decision: AppStatus is INITIAL and not going to auth. Redirecting to $authenticationPath to settle auth first.', - ); - return authenticationPath; - } - // For configFetching or configFetchFailed, or initial going to auth, - // let the App widget's builder handle the UI (loading/error screen). - print( - ' Redirect Decision: AppStatus is $appStatus. Allowing App widget to handle display or navigation to auth.', - ); - return null; - } + // With the new App startup architecture, the router is only active when + // the app is in a stable, running state. The `redirect` function's + // only responsibility is to handle auth-based route protection. + // States like `configFetching`, `underMaintenance`, etc., are now + // handled by the root App widget *before* this router is ever built. - // --- Case 1: Unauthenticated User (after initial phase, config not relevant yet for this decision) --- + // --- Case 1: Unauthenticated User --- + // If the user is unauthenticated, they should be on an auth path. + // If they are trying to access any other part of the app, redirect them. if (appStatus == AppStatus.unauthenticated) { - print(' Redirect Decision: User is UNauthenticated.'); - if (!isGoingToAuth) { - print( - ' Action: Not going to auth. Redirecting to $authenticationPath', - ); - return authenticationPath; - } - print(' Action: Already going to auth. Allowing navigation.'); - return null; + print(' Redirect: User is unauthenticated.'); + // If they are already on an auth path, allow it. Otherwise, redirect. + return isGoingToAuth ? null : authenticationPath; } // --- Case 2: Anonymous or Authenticated User --- - // (Covers AppStatus.anonymous and AppStatus.authenticated) - // At this point, AppConfig should be loaded or its loading/error state is handled by App widget. - // The main concern here is preventing authenticated users from re-entering basic auth flows. + // If a user is anonymous or authenticated, they should not be able to + // access the main authentication flows, with an exception for account + // linking for anonymous users. if (appStatus == AppStatus.anonymous || appStatus == AppStatus.authenticated) { - print(' Redirect Decision: User is $appStatus.'); + print(' Redirect: User is $appStatus.'); - final isLinkingContextQueryPresent = - state.uri.queryParameters['context'] == 'linking'; - final isLinkingPathSegmentPresent = currentLocation.contains( - '/linking/', - ); - - // Determine if the current location is part of any linking flow (either via query or path segment) - final isAnyLinkingContext = - isLinkingContextQueryPresent || isLinkingPathSegmentPresent; - - // If an authenticated/anonymous user is on any authentication-related path: - if (currentLocation.startsWith(authenticationPath)) { - print( - ' Debug: Auth path detected. Current Location: $currentLocation', - ); - print( - ' Debug: URI Query Parameters: ${state.uri.queryParameters}', - ); - print( - ' Debug: isLinkingContextQueryPresent: $isLinkingContextQueryPresent', - ); - print( - ' Debug: isLinkingPathSegmentPresent: $isLinkingPathSegmentPresent', - ); - print( - ' Debug: isAnyLinkingContext evaluated to: $isAnyLinkingContext', - ); - - // If the user is authenticated, always redirect away from auth paths. + // If the user is trying to access an authentication path: + if (isGoingToAuth) { + // A fully authenticated user should never see auth pages. if (appStatus == AppStatus.authenticated) { - print( - ' Action: Authenticated user on auth path ($currentLocation). Redirecting to $feedPath', - ); + print(' Action: Authenticated user on auth path. Redirecting to feed.'); return feedPath; } - // If the user is anonymous, allow navigation within auth paths if in a linking context. - // Otherwise, redirect anonymous users trying to access non-linking auth paths to feed. - if (isAnyLinkingContext) { - print( - ' Action: Anonymous user on auth linking path ($currentLocation). Allowing navigation.', - ); + // An anonymous user is only allowed on auth paths for account linking. + final isLinking = + state.uri.queryParameters['context'] == 'linking' || + currentLocation.contains('/linking/'); + + if (isLinking) { + print(' Action: Anonymous user on linking path. Allowing.'); return null; } else { - print( - ' Action: Anonymous user trying to access non-linking auth path ($currentLocation). Redirecting to $feedPath', - ); + print(' Action: Anonymous user on non-linking auth path. Redirecting to feed.'); return feedPath; } } - // Allow access to other routes (non-auth paths) - print( - ' Action: Allowing navigation to $currentLocation for $appStatus user (non-auth path).', - ); - return null; } - // Fallback (should ideally not be reached if all statuses are handled) - print( - ' Redirect Decision: Fallback, no specific condition met for $appStatus. Allowing navigation.', - ); + // --- Fallback --- + // For any other case, allow navigation. + print(' Redirect: No condition met. Allowing navigation.'); return null; }, // --- Authentication Routes --- From 513063bc589c5cd325e4c21c0529f439cb105ec3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 08:50:03 +0100 Subject: [PATCH 05/12] refactor(theme): centralize theme customization in app.dart - Add theme customization parameters in app.dart - Update import statements for l10n - Adjust theme settings in StatusPage - TODO: Implement proper localization for retry button text --- lib/app/view/app.dart | 43 +++++++++++++++++++++++++++----- lib/status/view/status_page.dart | 6 ++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 7ae6d91..083e84e 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -9,6 +9,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_e import 'package:flutter_news_app_mobile_client_full_source_code/app/services/app_status_service.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_migration_service.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/router/router.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart'; @@ -207,8 +208,18 @@ class _AppViewState extends State<_AppView> { // It's wrapped in a basic MaterialApp to provide theme and l10n. return MaterialApp( debugShowCheckedModeBanner: false, - theme: lightTheme(), - darkTheme: darkTheme(), + theme: lightTheme( + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, + ), + darkTheme: darkTheme( + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, + ), themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, @@ -220,8 +231,18 @@ class _AppViewState extends State<_AppView> { // A mandatory update is required. Show the UpdateRequiredPage. return MaterialApp( debugShowCheckedModeBanner: false, - theme: lightTheme(), - darkTheme: darkTheme(), + theme: lightTheme( + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, + ), + darkTheme: darkTheme( + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, + ), themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, @@ -236,8 +257,18 @@ class _AppViewState extends State<_AppView> { // both the loading indicator and the retry mechanism. return MaterialApp( debugShowCheckedModeBanner: false, - theme: lightTheme(), - darkTheme: darkTheme(), + theme: lightTheme( + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, + ), + darkTheme: darkTheme( + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, + ), themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, diff --git a/lib/status/view/status_page.dart b/lib/status/view/status_page.dart index b4a8752..e477245 100644 --- a/lib/status/view/status_page.dart +++ b/lib/status/view/status_page.dart @@ -1,4 +1,4 @@ -import 'package:core/core.dart'; +import 'package:core/core.dart' hide AppStatus; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; @@ -24,7 +24,7 @@ class StatusPage extends StatelessWidget { return Scaffold( body: BlocBuilder( builder: (context, state) { - final l10n = context.l10n; + final l10n = AppLocalizationsX(context).l10n; if (state.status == AppStatus.configFetching) { // While fetching configuration, display a clear loading indicator. @@ -40,7 +40,7 @@ class StatusPage extends StatelessWidget { // This allows the user to recover from transient network issues. return FailureStateWidget( exception: const NetworkException(), // A generic network error - retryButtonText: l10n.retryButtonText, + retryButtonText: 'l10n.retryButtonText', //TODO(fulleni): localize me. onRetry: () { // Dispatch the event to AppBloc to re-trigger the fetch. context.read().add(const AppConfigFetchRequested()); From a79e439075bfbfdfbdb0fcfd887a9597ab72d853 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 10:28:02 +0100 Subject: [PATCH 06/12] refactor(app): improve comments and structure in _AppViewState - Enhance comments to explain the purpose and functionality of key widgets - Reorganize comment sections for better clarity and flow - Add explanations for the use of separate MaterialApp widgets - Clarify the theme configuration process for different app states - Emphasize the importance of the stable main application UI root --- lib/app/view/app.dart | 49 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 083e84e..101abe5 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -184,17 +184,22 @@ class _AppViewState extends State<_AppView> { @override Widget build(BuildContext context) { // Wrap the part of the tree that needs to react to AppBloc state changes - // (specifically for updating the ValueNotifier) with a BlocListener. - // The BlocBuilder remains for theme changes. + // with a BlocListener and a BlocBuilder. return BlocListener( - // Listen for status changes to update the GoRouter's ValueNotifier + // The BlocListener's primary role here is to keep GoRouter's refresh + // mechanism informed about authentication status changes. + // GoRouter's `redirect` logic depends on this notifier to re-evaluate + // routes when the user logs in or out *while the app is running*. listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { _statusNotifier.value = state.status; }, // The BlocBuilder is the core of the new stable startup architecture. - // It acts as a high-level switch that determines which UI to show based - // on the application's status. + // It functions as a "master switch" for the entire application's UI. + // Based on the AppStatus, it decides whether to show a full-screen + // status page (like Maintenance or Loading) or to build the main + // application UI with its nested router. This approach is fundamental + // to fixing the original race conditions and BuildContext instability. child: BlocBuilder( builder: (context, state) { // --- Full-Screen Status Pages --- @@ -205,7 +210,19 @@ class _AppViewState extends State<_AppView> { if (state.status == AppStatus.underMaintenance) { // The app is in maintenance mode. Show the MaintenancePage. - // It's wrapped in a basic MaterialApp to provide theme and l10n. + // + // WHY A SEPARATE MATERIALAPP? + // Each status page is wrapped in its own simple MaterialApp to create + // a self-contained environment. This provides the necessary + // Directionality, theme, and localization context for the page + // to render correctly, without needing the main app's router. + // + // WHY A DEFAULT THEME? + // The theme uses hardcoded, sensible defaults (like FlexScheme.material) + // because at this early stage, we only need a basic visual structure. + // However, we critically use `state.themeMode` and `state.locale`, + // which are loaded from user settings *before* the maintenance check, + // ensuring the page respects the user's chosen light/dark mode and language. return MaterialApp( debugShowCheckedModeBanner: false, theme: lightTheme( @@ -223,6 +240,7 @@ class _AppViewState extends State<_AppView> { themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, + locale: state.locale, home: const MaintenancePage(), ); } @@ -246,6 +264,7 @@ class _AppViewState extends State<_AppView> { themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, + locale: state.locale, home: const UpdateRequiredPage(), ); } @@ -272,6 +291,7 @@ class _AppViewState extends State<_AppView> { themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, + locale: state.locale, home: const StatusPage(), ); } @@ -279,11 +299,20 @@ class _AppViewState extends State<_AppView> { // --- Main Application UI --- // If none of the critical states above are met, the app is ready // to display its main UI. We build the MaterialApp.router here. + // This is the single, STABLE root widget for the entire main app. // - // This is the STABLE root of the main application. Because it is - // only built when the app is in a "running" state, it will not be - // destroyed and rebuilt during startup, which fixes the - // `BuildContext` instability and related crashes. + // WHY IS THIS SO IMPORTANT? + // Because this widget is now built conditionally inside a single + // BlocBuilder, it is created only ONCE when the app enters a + // "running" state (e.g., authenticated, anonymous). It is no longer + // destroyed and rebuilt during startup, which was the root cause of + // the `BuildContext` instability and the `l10n` crashes. + // + // THEME CONFIGURATION: + // Unlike the status pages, this MaterialApp is themed using the full, + // detailed settings loaded into the AppState (e.g., `state.flexScheme`, + // `state.settings.displaySettings...`), providing the complete, + // personalized user experience. return MaterialApp.router( debugShowCheckedModeBanner: false, themeMode: state.themeMode, From a52dacc2316fc597d444d0227466a24fdc1b7bc0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 10:28:41 +0100 Subject: [PATCH 07/12] refactor(app): remove redundant remote config fetch check - Removed the conditional check that prevented refetching remote config for the current user session - This change allows for more frequent updates to remote config without being restricted to error recovery scenarios --- lib/app/bloc/app_bloc.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 2de32ef..0b43ec9 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -411,15 +411,6 @@ class AppBloc extends Bloc { return; } - // Avoid refetching if already loaded for the current user session, unless explicitly trying to recover from a failed state. - if (state.remoteConfig != null && - state.status != AppStatus.configFetchFailed) { - print( - '[AppBloc] AppConfig already loaded for user ${state.user?.id} and not in a failed state. Skipping fetch.', - ); - return; - } - print( '[AppBloc] Attempting to fetch AppConfig for user: ${state.user!.id}...', ); From bc894c699b81b8cc064afb8d9a68bb5e9afbfc48 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 10:28:56 +0100 Subject: [PATCH 08/12] fix(router): improve initial route handling and redirect logic - Add root path as initial location to prevent complex page builds - Implement redirect logic from root to appropriate path - Add comments explaining the routing changes --- lib/router/router.dart | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index ecb110a..9106ca6 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -73,7 +73,10 @@ GoRouter createRouter({ return GoRouter( refreshListenable: authStatusNotifier, - initialLocation: Routes.feed, + // Start at a neutral root path. The redirect logic will immediately + // determine the correct path (/feed or /authentication), preventing + // an attempt to build a complex page before the app state is ready. + initialLocation: '/', debugLogDiagnostics: true, // --- Redirect Logic --- redirect: (BuildContext context, GoRouterState state) { @@ -87,6 +90,7 @@ GoRouter createRouter({ ); // --- Define Key Paths --- + const rootPath = '/'; const authenticationPath = Routes.authentication; const feedPath = Routes.feed; final isGoingToAuth = currentLocation.startsWith(authenticationPath); @@ -135,6 +139,12 @@ GoRouter createRouter({ return feedPath; } } + + // If the user is at the root path, they should be sent to the feed. + if (currentLocation == rootPath) { + print(' Action: User at root. Redirecting to feed.'); + return feedPath; + } } // --- Fallback --- @@ -144,6 +154,13 @@ GoRouter createRouter({ }, // --- Authentication Routes --- routes: [ + // A neutral root route that the app starts on. The redirect logic will + // immediately move the user to the correct location. This route's + // builder will never be called in practice. + GoRoute( + path: '/', + builder: (context, state) => const SizedBox.shrink(), + ), GoRoute( path: Routes.authentication, name: Routes.authenticationName, From ea3858f1c8fa5639d0cfc3308228452ecdc17a27 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 10:36:41 +0100 Subject: [PATCH 09/12] feat(app): add demo mode optimizations - Add environment check to skip periodic app status checks in demo mode - Implement conditional app lifecycle checks based on environment - Update AppStatusService constructor to include AppEnvironment parameter - Modify _AppViewState to pass environment to AppStatusService These changes improve the demo experience by reducing unnecessary reloads and optimizing resource usage when running in demo mode. --- lib/app/services/app_status_service.dart | 23 ++++++++++++++++++++++- lib/app/view/app.dart | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/app/services/app_status_service.dart b/lib/app/services/app_status_service.dart index a27f4cc..b67cbcd 100644 --- a/lib/app/services/app_status_service.dart +++ b/lib/app/services/app_status_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/app/config/config.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; /// {@template app_status_service} @@ -28,8 +29,10 @@ class AppStatusService with WidgetsBindingObserver { AppStatusService({ required BuildContext context, required Duration checkInterval, + required AppEnvironment environment, }) : _context = context, - _checkInterval = checkInterval { + _checkInterval = checkInterval, + _environment = environment { // Immediately register this service as a lifecycle observer. WidgetsBinding.instance.addObserver(this); // Start the periodic checks. @@ -42,6 +45,9 @@ class AppStatusService with WidgetsBindingObserver { /// The interval at which to perform periodic status checks. final Duration _checkInterval; + /// The current application environment. + final AppEnvironment _environment; + /// The timer responsible for periodic checks. Timer? _timer; @@ -54,6 +60,13 @@ class AppStatusService with WidgetsBindingObserver { _timer?.cancel(); // Create a new periodic timer. _timer = Timer.periodic(_checkInterval, (_) { + // In demo mode, periodic checks are not needed as there's no backend. + if (_environment == AppEnvironment.demo) { + print( + '[AppStatusService] Demo mode: Skipping periodic check.', + ); + return; + } print( '[AppStatusService] Periodic check triggered. Requesting AppConfig fetch.', ); @@ -67,6 +80,14 @@ class AppStatusService with WidgetsBindingObserver { /// This method is called whenever the application's lifecycle state changes. @override void didChangeAppLifecycleState(AppLifecycleState state) { + // In demo mode, we disable the app resume check. This is especially + // useful on web, where switching browser tabs would otherwise trigger + // a reload, which is unnecessary and can be distracting for demos. + if (_environment == AppEnvironment.demo) { + print('[AppStatusService] Demo mode: Skipping app lifecycle check.'); + return; + } + // We are only interested in the 'resumed' state. if (state == AppLifecycleState.resumed) { print( diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 101abe5..c4d64b9 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -152,6 +152,7 @@ class _AppViewState extends State<_AppView> { _appStatusService = AppStatusService( context: context, checkInterval: const Duration(minutes: 15), + environment: widget.environment, ); _router = createRouter( From 5ff70a0b70f06a9dcc6b3aae3eef4e57686034e7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 10:54:27 +0100 Subject: [PATCH 10/12] refactor(app): improve AppConfig fetching logic - Introduce background check option for AppConfig fetching - Optimize state transitions and error handling - Simplify event classes for better maintainability - Enhance logging for background fetch operations --- lib/app/bloc/app_bloc.dart | 82 ++++++++++++--------- lib/app/bloc/app_event.dart | 94 +++++++----------------- lib/app/services/app_status_service.dart | 8 +- 3 files changed, 79 insertions(+), 105 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 0b43ec9..44c77d2 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -411,16 +411,20 @@ class AppBloc extends Bloc { return; } - print( - '[AppBloc] Attempting to fetch AppConfig for user: ${state.user!.id}...', - ); - emit( - state.copyWith( - status: AppStatus.configFetching, - remoteConfig: null, - clearAppConfig: true, - ), - ); + // For background checks, we don't want to show a loading screen. + // Only for the initial fetch should we set the status to configFetching. + if (!event.isBackgroundCheck) { + print( + '[AppBloc] Initial config fetch. Setting status to configFetching.', + ); + emit( + state.copyWith( + status: AppStatus.configFetching, + ), + ); + } else { + print('[AppBloc] Background config fetch. Proceeding silently.'); + } try { final remoteConfig = await _appConfigRepository.read(id: kRemoteConfigId); @@ -428,11 +432,10 @@ class AppBloc extends Bloc { '[AppBloc] Remote Config fetched successfully. ID: ${remoteConfig.id} for user: ${state.user!.id}', ); - // --- CRITICAL STARTUP SEQUENCE EVALUATION --- - // After successfully fetching the remote configuration, we must - // evaluate the app's status in a specific order before allowing - // the main UI to be built. - + // --- CRITICAL STATUS EVALUATION --- + // For both initial and background checks, if a critical status is found, + // we must update the app status immediately to lock the UI. + // 1. Check for Maintenance Mode. This has the highest priority. if (remoteConfig.appStatus.isUnderMaintenance) { emit( @@ -456,36 +459,43 @@ class AppBloc extends Bloc { return; } - // 3. If no critical status is active, proceed to the normal app state. - // The status is determined by the user's role (authenticated/anonymous). - final finalStatus = state.user!.appRole == AppUserRole.standardUser - ? AppStatus.authenticated - : AppStatus.anonymous; - - emit(state.copyWith(remoteConfig: remoteConfig, status: finalStatus)); + // --- POST-CHECK STATE RESOLUTION --- + // If no critical status was found, we resolve the final state. + + // For an initial fetch, we transition from configFetching to the correct + // authenticated/anonymous state. + if (!event.isBackgroundCheck) { + final finalStatus = state.user!.appRole == AppUserRole.standardUser + ? AppStatus.authenticated + : AppStatus.anonymous; + emit(state.copyWith(remoteConfig: remoteConfig, status: finalStatus)); + } else { + // For a background check, the status is already correct (e.g., authenticated). + // We just need to update the remoteConfig in the state silently. + // The status does not need to change, preventing a disruptive UI rebuild. + emit(state.copyWith(remoteConfig: remoteConfig)); + } } on HttpException catch (e) { print( '[AppBloc] Failed to fetch AppConfig (HttpException) for user ${state.user?.id}: ${e.runtimeType} - ${e.message}', ); - emit( - state.copyWith( - status: AppStatus.configFetchFailed, - remoteConfig: null, - clearAppConfig: true, - ), - ); + // Only show a failure screen on an initial fetch. + // For background checks, we fail silently to avoid disruption. + if (!event.isBackgroundCheck) { + emit(state.copyWith(status: AppStatus.configFetchFailed)); + } else { + print('[AppBloc] Silent failure on background config fetch.'); + } } catch (e, s) { print( '[AppBloc] Unexpected error fetching AppConfig for user ${state.user?.id}: $e', ); print('[AppBloc] Stacktrace: $s'); - emit( - state.copyWith( - status: AppStatus.configFetchFailed, - remoteConfig: null, - clearAppConfig: true, - ), - ); + if (!event.isBackgroundCheck) { + emit(state.copyWith(status: AppStatus.configFetchFailed)); + } else { + print('[AppBloc] Silent failure on background config fetch.'); + } } } diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index 33fd3ee..306c23f 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -7,6 +7,7 @@ abstract class AppEvent extends Equatable { List get props => []; } +/// Dispatched when the authentication state changes (e.g., user logs in/out). class AppUserChanged extends AppEvent { const AppUserChanged(this.user); @@ -16,122 +17,81 @@ class AppUserChanged extends AppEvent { List get props => [user]; } -/// {@template app_settings_refreshed} -/// Internal event to trigger reloading of settings within AppBloc. -/// Added when user changes or upon explicit request. -/// {@endtemplate} +/// Dispatched to request a refresh of the user's application settings. class AppSettingsRefreshed extends AppEvent { - /// {@macro app_settings_refreshed} const AppSettingsRefreshed(); } -/// {@template app_logout_requested} -/// Event to request user logout. -/// {@endtemplate} +/// Dispatched to fetch the remote application configuration. +class AppConfigFetchRequested extends AppEvent { + const AppConfigFetchRequested({this.isBackgroundCheck = false}); + + /// Whether this fetch is a silent background check. + /// + /// If `true`, the BLoC will not enter a visible loading state. + /// If `false` (default), it's treated as an initial fetch that shows a + /// loading UI. + final bool isBackgroundCheck; + + @override + List get props => [isBackgroundCheck]; +} + +/// Dispatched when the user logs out. class AppLogoutRequested extends AppEvent { - /// {@macro app_logout_requested} const AppLogoutRequested(); } -/// {@template app_theme_mode_changed} -/// Event to change the application's theme mode. -/// {@endtemplate} +/// Dispatched when the theme mode (light/dark/system) changes. class AppThemeModeChanged extends AppEvent { - /// {@macro app_theme_mode_changed} const AppThemeModeChanged(this.themeMode); - final ThemeMode themeMode; - @override - List get props => [themeMode]; + List get props => [themeMode]; } -/// {@template app_flex_scheme_changed} -/// Event to change the application's FlexColorScheme. -/// {@endtemplate} +/// Dispatched when the accent color theme changes. class AppFlexSchemeChanged extends AppEvent { - /// {@macro app_flex_scheme_changed} const AppFlexSchemeChanged(this.flexScheme); - final FlexScheme flexScheme; - @override - List get props => [flexScheme]; + List get props => [flexScheme]; } -/// {@template app_font_family_changed} -/// Event to change the application's font family. -/// {@endtemplate} +/// Dispatched when the font family changes. class AppFontFamilyChanged extends AppEvent { - /// {@macro app_font_family_changed} const AppFontFamilyChanged(this.fontFamily); - final String? fontFamily; - @override List get props => [fontFamily]; } -/// {@template app_text_scale_factor_changed} -/// Event to change the application's text scale factor. -/// {@endtemplate} +/// Dispatched when the text scale factor changes. class AppTextScaleFactorChanged extends AppEvent { - /// {@macro app_text_scale_factor_changed} const AppTextScaleFactorChanged(this.appTextScaleFactor); - final AppTextScaleFactor appTextScaleFactor; - @override - List get props => [appTextScaleFactor]; + List get props => [appTextScaleFactor]; } -/// {@template app_font_weight_changed} -/// Event to change the application's font weight. -/// {@endtemplate} +/// Dispatched when the font weight changes. class AppFontWeightChanged extends AppEvent { - /// {@macro app_font_weight_changed} const AppFontWeightChanged(this.fontWeight); - - /// The new font weight to apply. final AppFontWeight fontWeight; - @override List get props => [fontWeight]; } -/// {@template app_config_fetch_requested} -/// Event to trigger fetching of the global AppConfig. -/// {@endtemplate} -class AppConfigFetchRequested extends AppEvent { - /// {@macro app_config_fetch_requested} - const AppConfigFetchRequested(); -} - -/// {@template app_opened} -/// Event triggered when the application is opened. -/// Used to check for required updates or maintenance mode. -/// {@endtemplate} -class AppOpened extends AppEvent { - /// {@macro app_opened} - const AppOpened(); -} - -/// {@template app_user_account_action_shown} -/// Event triggered when an AccountAction has been shown to the user, -/// prompting an update to their `lastAccountActionShownAt` timestamp. -/// {@endtemplate} +/// Dispatched when a one-time user account action has been shown. class AppUserAccountActionShown extends AppEvent { - /// {@macro app_user_account_action_shown} const AppUserAccountActionShown({ required this.userId, required this.feedActionType, required this.isCompleted, }); - final String userId; final FeedActionType feedActionType; final bool isCompleted; - @override List get props => [userId, feedActionType, isCompleted]; } diff --git a/lib/app/services/app_status_service.dart b/lib/app/services/app_status_service.dart index b67cbcd..1871525 100644 --- a/lib/app/services/app_status_service.dart +++ b/lib/app/services/app_status_service.dart @@ -71,7 +71,9 @@ class AppStatusService with WidgetsBindingObserver { '[AppStatusService] Periodic check triggered. Requesting AppConfig fetch.', ); // Add the event to the AppBloc to fetch the latest config. - _context.read().add(const AppConfigFetchRequested()); + _context + .read() + .add(const AppConfigFetchRequested(isBackgroundCheck: true)); }); } @@ -96,7 +98,9 @@ class AppStatusService with WidgetsBindingObserver { // When the app comes to the foreground, immediately trigger a check. // This is crucial for catching maintenance mode that was enabled // while the app was in the background. - _context.read().add(const AppConfigFetchRequested()); + _context + .read() + .add(const AppConfigFetchRequested(isBackgroundCheck: true)); } } From d77a4193fb09e8e91ee39aa9f6937d2a467df443 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 11:04:33 +0100 Subject: [PATCH 11/12] docs(README): add section on backend-driven behavior features - Explain maintenance mode and forced update functionalities - Highlight the advantage of dynamic control over user experience - Emphasize the pre-implemented logic for graceful server downtime and version enforcement --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ed47538..9220b43 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,12 @@ Offer users control over their app experience: * **Feed Display:** Customize how headlines are presented. > **Your Advantage:** Deliver a premium, adaptable user experience that caters to individual needs without writing any code. 🔧 +#### 📡 **Backend-Driven Behavior** +The app is built to respond to commands from your backend API, allowing for dynamic control over the user experience: +* **Maintenance Mode:** Displays a full-screen "kill switch" page when the backend signals that the service is temporarily unavailable. +* **Forced Updates:** Shows a non-dismissible "Update Required" screen when a new version is mandatory, guiding users to the app store. +> **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. 🛠️ + #### 📱 **Adaptive UI for All Screens** Built with `flutter_adaptive_scaffold`, the app offers responsive navigation and layouts that look great on both phones and tablets. > **Your Advantage:** Deliver a consistent and optimized UX across a wide range of devices effortlessly. ↔️ From 714a1c1673b1273ff2c5c9658ec2bf97612ca3bf Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 7 Aug 2025 11:04:51 +0100 Subject: [PATCH 12/12] lint: misc --- lib/app/services/app_status_service.dart | 2 +- lib/app/view/app.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/app/services/app_status_service.dart b/lib/app/services/app_status_service.dart index 1871525..2a2bb7a 100644 --- a/lib/app/services/app_status_service.dart +++ b/lib/app/services/app_status_service.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_news_app_mobile_client_full_source_code/app/config/config.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/app/config/config.dart'; /// {@template app_status_service} /// A service dedicated to monitoring the application's lifecycle and diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index c4d64b9..029c37a 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -10,7 +10,6 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/services/app import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_migration_service.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart'; -import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/router/router.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart'; import 'package:go_router/go_router.dart';