diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 452b1907..d21eb6ca 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -18,6 +18,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/router/router.da import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart'; import 'package:go_router/go_router.dart'; import 'package:kv_storage_service/kv_storage_service.dart'; +import 'package:logging/logging.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template app_widget} @@ -223,6 +224,7 @@ class _AppViewState extends State<_AppView> { late final GoRouter _router; late final ValueNotifier _statusNotifier; AppStatusService? _appStatusService; + final _routerLogger = Logger('GoRouter'); @override void initState() { @@ -257,6 +259,7 @@ class _AppViewState extends State<_AppView> { adService: widget.adService, navigatorKey: widget.navigatorKey, inlineAdCacheService: widget.inlineAdCacheService, + logger: _routerLogger, ); } diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 6836de66..040f2e7a 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -39,14 +39,37 @@ Future bootstrap( app_config.AppConfig appConfig, app_config.AppEnvironment environment, ) async { + // Setup logging + Logger.root.level = environment == app_config.AppEnvironment.production + ? Level.INFO + : Level.ALL; + + Logger.root.onRecord.listen((record) { + final message = StringBuffer( + '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}', + ); + if (record.error != null) { + message.write('\nError: ${record.error}'); + } + if (record.stackTrace != null) { + message.write('\nStack Trace: ${record.stackTrace}'); + } + print(message); + }); + + final logger = Logger('bootstrap') + ..config('--- Starting Bootstrap Process ---') + ..config('App Environment: $environment'); + WidgetsFlutterBinding.ensureInitialized(); Bloc.observer = const AppBlocObserver(); - final logger = Logger('bootstrap'); timeago.setLocaleMessages('en', EnTimeagoMessages()); timeago.setLocaleMessages('ar', ArTimeagoMessages()); + logger.info('1. Initializing KV Storage Service...'); // 1. Initialize KV Storage Service first, as it's a foundational dependency. final kvStorage = await KVStorageSharedPreferences.getInstance(); + logger.fine('KV Storage Service initialized (SharedPreferences).'); // Initialize InlineAdCacheService early as it's a singleton and needs AdService. // It will be fully configured once AdService is available. @@ -54,6 +77,7 @@ Future bootstrap( // 2. Initialize HttpClient. Its tokenProvider now directly reads from // kvStorage, breaking the circular dependency with AuthRepository. + logger.info('2. Initializing HttpClient...'); // This HttpClient instance is used for all subsequent API calls, including // the initial unauthenticated fetch of RemoteConfig. final httpClient = HttpClient( @@ -62,11 +86,14 @@ Future bootstrap( kvStorage.readString(key: StorageKey.authToken.stringValue), logger: logger, ); - - // 3. Initialize RemoteConfigClient and Repository, and fetch RemoteConfig. + logger + ..fine('HttpClient initialized for base URL: ${appConfig.baseUrl}') + // 3. Initialize RemoteConfigClient and Repository, and fetch RemoteConfig. + ..info('3. Initializing RemoteConfig client and repository...'); // This is done early because RemoteConfig is now publicly accessible (unauthenticated). late DataClient remoteConfigClient; if (appConfig.environment == app_config.AppEnvironment.demo) { + logger.fine('Using in-memory client for RemoteConfig.'); remoteConfigClient = DataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, @@ -74,6 +101,7 @@ Future bootstrap( logger: logger, ); } else { + logger.fine('Using API client for RemoteConfig.'); // For development and production environments, use DataApi. remoteConfigClient = DataApi( httpClient: httpClient, @@ -86,6 +114,7 @@ Future bootstrap( final remoteConfigRepository = DataRepository( dataClient: remoteConfigClient, ); + logger.fine('RemoteConfig repository initialized.'); // Fetch the initial RemoteConfig. This is a critical step to determine // the app's global status (e.g., maintenance mode, update required) @@ -93,31 +122,28 @@ Future bootstrap( RemoteConfig? initialRemoteConfig; HttpException? initialRemoteConfigError; + logger.info('4. Fetching initial RemoteConfig...'); try { initialRemoteConfig = await remoteConfigRepository.read( id: kRemoteConfigId, ); - logger.info('[bootstrap] Initial RemoteConfig fetched successfully.'); + logger.fine('Initial RemoteConfig fetched successfully.'); } on HttpException catch (e) { - logger.severe( - '[bootstrap] Failed to fetch initial RemoteConfig (HttpException): $e', - ); + logger.severe('Failed to fetch initial RemoteConfig (HttpException): $e'); initialRemoteConfigError = e; } catch (e, s) { - logger.severe( - '[bootstrap] Unexpected error fetching initial RemoteConfig.', - e, - s, - ); + logger.severe('Unexpected error fetching initial RemoteConfig.', e, s); initialRemoteConfigError = UnknownException(e.toString()); } // 4. Conditionally initialize Auth services based on environment. // This is done after RemoteConfig is fetched, as Auth services might depend - // on configurations defined in RemoteConfig (though not directly in this case). + // on configurations defined in RemoteConfig. + logger.info('5. Initializing Authentication services...'); late final AuthClient authClient; late final AuthRepository authenticationRepository; if (appConfig.environment == app_config.AppEnvironment.demo) { + logger.fine('Using in-memory client for Authentication.'); // In-memory authentication for demo environment. authClient = AuthInmemory(); authenticationRepository = AuthRepository( @@ -125,6 +151,7 @@ Future bootstrap( storageService: kvStorage, ); } else { + logger.fine('Using API client for Authentication.'); // Now that httpClient is available, initialize AuthApi and AuthRepository. authClient = AuthApi(httpClient: httpClient); authenticationRepository = AuthRepository( @@ -132,14 +159,17 @@ Future bootstrap( storageService: kvStorage, ); } - - // 5. Initialize AdProvider and AdService. + logger + ..fine('Authentication repository initialized.') + // 5. Initialize AdProvider and AdService. + ..info('6. Initializing Ad providers and AdService...'); late final Map adProviders; // Conditionally instantiate ad providers based on the application environment. // This ensures that only the relevant ad providers are available for the // current environment, preventing unintended usage. if (appConfig.environment == app_config.AppEnvironment.demo || kIsWeb) { + logger.fine('Using DemoAdProvider for all ad platforms.'); final demoAdProvider = DemoAdProvider(logger: logger); adProviders = { // In the demo environment or on the web, all ad platform types map to @@ -151,6 +181,7 @@ Future bootstrap( AdPlatformType.demo: demoAdProvider, }; } else { + logger.fine('Using AdMobAdProvider and LocalAdProvider.'); // For development and production environments (non-web), use real ad providers. adProviders = { // AdMob provider for Google Mobile Ads. @@ -180,13 +211,17 @@ Future bootstrap( logger: logger, ); await adService.initialize(); + logger.fine('AdService initialized.'); // Initialize InlineAdCacheService with the created AdService. inlineAdCacheService = InlineAdCacheService(adService: adService); - - // Fetch the initial user from the authentication repository. - // This ensures the AppBloc starts with an accurate authentication status. + logger + ..fine('InlineAdCacheService initialized.') + // Fetch the initial user from the authentication repository. + // This ensures the AppBloc starts with an accurate authentication status. + ..info('7. Fetching initial user...'); final initialUser = await authenticationRepository.getCurrentUser(); + logger.fine('Initial user fetched: ${initialUser?.id ?? 'none'}.'); // Create a GlobalKey for the NavigatorState to be used by AppBloc // and InterstitialAdManager for BuildContext access. @@ -194,8 +229,10 @@ Future bootstrap( // Initialize PackageInfoService final packageInfoService = PackageInfoServiceImpl(logger: logger); - - // 6. Initialize all other DataClients and Repositories. + logger + ..fine('PackageInfoService initialized.') + // 6. Initialize all other DataClients and Repositories. + ..info('8. Initializing Data clients and repositories...'); // These now also have a guaranteed valid httpClient. late final DataClient headlinesClient; late final DataClient topicsClient; @@ -206,6 +243,7 @@ Future bootstrap( late final DataClient userClient; late final DataClient localAdClient; if (appConfig.environment == app_config.AppEnvironment.demo) { + logger.fine('Using in-memory clients for all data repositories.'); headlinesClient = DataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, @@ -280,6 +318,7 @@ Future bootstrap( logger: logger, ); } else if (appConfig.environment == app_config.AppEnvironment.development) { + logger.fine('Using API clients for all data repositories (Development).'); headlinesClient = DataApi( httpClient: httpClient, modelName: 'headline', @@ -337,6 +376,7 @@ Future bootstrap( logger: logger, ); } else { + logger.fine('Using API clients for all data repositories (Production).'); // Default to API clients for production headlinesClient = DataApi( httpClient: httpClient, @@ -395,6 +435,7 @@ Future bootstrap( logger: logger, ); } + logger.fine('All data clients instantiated.'); final headlinesRepository = DataRepository( dataClient: headlinesClient, @@ -413,6 +454,7 @@ Future bootstrap( dataClient: userAppSettingsClient, ); final userRepository = DataRepository(dataClient: userClient); + logger.fine('All data repositories initialized.'); // Conditionally instantiate DemoDataMigrationService final demoDataMigrationService = @@ -422,6 +464,9 @@ Future bootstrap( userContentPreferencesRepository: userContentPreferencesRepository, ) : null; + logger.fine( + 'DemoDataMigrationService initialized: ${demoDataMigrationService != null}', + ); // Conditionally instantiate DemoDataInitializerService // This service is responsible for ensuring that essential user-specific data @@ -435,7 +480,11 @@ Future bootstrap( userContentPreferencesRepository: userContentPreferencesRepository, ) : null; - + logger + ..fine( + 'DemoDataInitializerService initialized: ${demoDataInitializerService != null}', + ) + ..info('--- Bootstrap Process Complete. Returning App widget. ---'); return App( authenticationRepository: authenticationRepository, headlinesRepository: headlinesRepository, diff --git a/lib/router/go_router_observer.dart b/lib/router/go_router_observer.dart new file mode 100644 index 00000000..bf11f0da --- /dev/null +++ b/lib/router/go_router_observer.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; + +class GoRouterObserver extends NavigatorObserver { + GoRouterObserver({required this.logger}); + + final Logger logger; + + @override + void didPush(Route route, Route? previousRoute) { + logger.info( + 'Pushed: ${route.settings.name} | from: ${previousRoute?.settings.name}', + ); + } + + @override + void didPop(Route route, Route? previousRoute) { + logger.info( + 'Popped: ${route.settings.name} | to: ${previousRoute?.settings.name}', + ); + } + + @override + void didRemove(Route route, Route? previousRoute) { + logger.info( + 'Removed: ${route.settings.name} | previous: ${previousRoute?.settings.name}', + ); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + logger.info( + 'Replaced: ${oldRoute?.settings.name} | with: ${newRoute?.settings.name}', + ); + } +} diff --git a/lib/router/router.dart b/lib/router/router.dart index 5444b791..330f7a8b 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -37,6 +37,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/v import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/bloc/headlines_search_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/view/headlines_search_page.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/go_router_observer.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/settings/bloc/settings_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/settings/view/appearance_settings_page.dart'; @@ -48,6 +49,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/settings/view/se import 'package:flutter_news_app_mobile_client_full_source_code/settings/view/theme_settings_page.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/shared/services/feed_decorator_service.dart'; import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; /// Creates and configures the GoRouter instance for the application. /// @@ -76,6 +78,7 @@ GoRouter createRouter({ required AdService adService, required GlobalKey navigatorKey, required InlineAdCacheService inlineAdCacheService, + required Logger logger, }) { // Instantiate FeedDecoratorService once to be shared final feedDecoratorService = FeedDecoratorService( @@ -91,9 +94,7 @@ GoRouter createRouter({ initialLocation: '/', debugLogDiagnostics: true, navigatorKey: navigatorKey, - observers: [ - // Add any other necessary observers here. If none, this can be an empty list. - ], + observers: [GoRouterObserver(logger: logger)], // --- Redirect Logic --- // This function is the single source of truth for route protection. // It's driven by the AppBloc's AppLifeCycleStatus. @@ -101,7 +102,7 @@ GoRouter createRouter({ final appStatus = context.read().state.status; final currentLocation = state.matchedLocation; - print( + logger.info( 'GoRouter Redirect Check:\n' ' Current Location (Matched): $currentLocation\n' ' AppStatus: $appStatus', @@ -118,24 +119,24 @@ GoRouter createRouter({ // If the user is unauthenticated, they must be on an auth path. // If they try to go anywhere else, they are redirected to the sign-in page. if (appStatus == AppLifeCycleStatus.unauthenticated) { - print(' Redirect: User is unauthenticated.'); + logger.info(' Redirect: User is unauthenticated.'); return isGoingToAuth ? null : authenticationPath; } // --- Case 2: Anonymous User --- // An anonymous user is partially authenticated. They can browse the app. if (appStatus == AppLifeCycleStatus.anonymous) { - print(' Redirect: User is anonymous.'); + logger.info(' Redirect: User is anonymous.'); // Block anonymous users from the main sign-in page. if (isGoingToAuth) { - print( + logger.info( ' Action: Anonymous user on auth path. Redirecting to feed.', ); return feedPath; } // If at the root, send them to the feed. if (currentLocation == rootPath) { - print(' Action: User at root. Redirecting to feed.'); + logger.info(' Action: User at root. Redirecting to feed.'); return feedPath; } // Allow navigation to other pages, including the new linking page. @@ -145,23 +146,23 @@ GoRouter createRouter({ // --- Case 3: Authenticated User --- // A fully authenticated user should be blocked from all auth/linking pages. if (appStatus == AppLifeCycleStatus.authenticated) { - print(' Redirect: User is authenticated.'); + logger.info(' Redirect: User is authenticated.'); if (isGoingToAuth || isGoingToLinking) { - print( + logger.info( ' Action: Authenticated user on auth/linking path. Redirecting to feed.', ); return feedPath; } // If at the root, send them to the feed. if (currentLocation == rootPath) { - print(' Action: User at root. Redirecting to feed.'); + logger.info(' Action: User at root. Redirecting to feed.'); return feedPath; } } // --- Fallback --- // For any other case (or if no conditions are met), allow navigation. - print(' Redirect: No condition met. Allowing navigation.'); + logger.info(' Redirect: No condition met. Allowing navigation.'); return null; }, // --- Authentication Routes --- @@ -551,8 +552,8 @@ GoRouter createRouter({ ); } else { // Handle case where user is unexpectedly null. - print( - 'ShellRoute/SettingsBloc: User ID is null when creating SettingsBloc. Settings will not be loaded.', + logger.warning( + 'User ID is null when creating SettingsBloc. Settings will not be loaded.', ); } return settingsBloc;