Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -223,6 +224,7 @@ class _AppViewState extends State<_AppView> {
late final GoRouter _router;
late final ValueNotifier<AppLifeCycleStatus> _statusNotifier;
AppStatusService? _appStatusService;
final _routerLogger = Logger('GoRouter');

@override
void initState() {
Expand Down Expand Up @@ -257,6 +259,7 @@ class _AppViewState extends State<_AppView> {
adService: widget.adService,
navigatorKey: widget.navigatorKey,
inlineAdCacheService: widget.inlineAdCacheService,
logger: _routerLogger,
);
}

Expand Down
68 changes: 57 additions & 11 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,42 @@ Future<Widget> 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) {
print(
'${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}',
);
if (record.error != null) {
print('Error: ${record.error}');
}
if (record.stackTrace != null) {
print('Stack Trace: ${record.stackTrace}');
}
});
final logger = Logger('bootstrap');
logger.config('--- Starting Bootstrap Process ---');
logger.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.
InlineAdCacheService? inlineAdCacheService;

// 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(
Expand All @@ -62,18 +83,22 @@ Future<Widget> bootstrap(
kvStorage.readString(key: StorageKey.authToken.stringValue),
logger: logger,
);
logger.fine('HttpClient initialized for base URL: ${appConfig.baseUrl}');

// 3. Initialize RemoteConfigClient and Repository, and fetch RemoteConfig.
logger.info('3. Initializing RemoteConfig client and repository...');
// This is done early because RemoteConfig is now publicly accessible (unauthenticated).
late DataClient<RemoteConfig> remoteConfigClient;
if (appConfig.environment == app_config.AppEnvironment.demo) {
logger.fine('Using in-memory client for RemoteConfig.');
remoteConfigClient = DataInMemory<RemoteConfig>(
toJson: (i) => i.toJson(),
getId: (i) => i.id,
initialData: remoteConfigsFixturesData,
logger: logger,
);
} else {
logger.fine('Using API client for RemoteConfig.');
// For development and production environments, use DataApi.
remoteConfigClient = DataApi<RemoteConfig>(
httpClient: httpClient,
Expand All @@ -86,60 +111,62 @@ Future<Widget> bootstrap(
final remoteConfigRepository = DataRepository<RemoteConfig>(
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)
// before proceeding with other initializations.
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(
authClient: authClient,
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(
authClient: authClient,
storageService: kvStorage,
);
}
logger.fine('Authentication repository initialized.');

// 5. Initialize AdProvider and AdService.
logger.info('6. Initializing Ad providers and AdService...');
late final Map<AdPlatformType, AdProvider> 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
Expand All @@ -151,6 +178,7 @@ Future<Widget> 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.
Expand Down Expand Up @@ -180,22 +208,28 @@ Future<Widget> bootstrap(
logger: logger,
);
await adService.initialize();
logger.fine('AdService initialized.');

// Initialize InlineAdCacheService with the created AdService.
inlineAdCacheService = InlineAdCacheService(adService: adService);
logger.fine('InlineAdCacheService initialized.');

// Fetch the initial user from the authentication repository.
// This ensures the AppBloc starts with an accurate authentication status.
logger.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.
final navigatorKey = GlobalKey<NavigatorState>();

// Initialize PackageInfoService
final packageInfoService = PackageInfoServiceImpl(logger: logger);
logger.fine('PackageInfoService initialized.');

// 6. Initialize all other DataClients and Repositories.
logger.info('8. Initializing Data clients and repositories...');
// These now also have a guaranteed valid httpClient.
late final DataClient<Headline> headlinesClient;
late final DataClient<Topic> topicsClient;
Expand All @@ -206,6 +240,7 @@ Future<Widget> bootstrap(
late final DataClient<User> userClient;
late final DataClient<LocalAd> localAdClient;
if (appConfig.environment == app_config.AppEnvironment.demo) {
logger.fine('Using in-memory clients for all data repositories.');
headlinesClient = DataInMemory<Headline>(
toJson: (i) => i.toJson(),
getId: (i) => i.id,
Expand Down Expand Up @@ -280,6 +315,7 @@ Future<Widget> bootstrap(
logger: logger,
);
} else if (appConfig.environment == app_config.AppEnvironment.development) {
logger.fine('Using API clients for all data repositories (Development).');
headlinesClient = DataApi<Headline>(
httpClient: httpClient,
modelName: 'headline',
Expand Down Expand Up @@ -337,6 +373,7 @@ Future<Widget> bootstrap(
logger: logger,
);
} else {
logger.fine('Using API clients for all data repositories (Production).');
// Default to API clients for production
headlinesClient = DataApi<Headline>(
httpClient: httpClient,
Expand Down Expand Up @@ -395,6 +432,7 @@ Future<Widget> bootstrap(
logger: logger,
);
}
logger.fine('All data clients instantiated.');

final headlinesRepository = DataRepository<Headline>(
dataClient: headlinesClient,
Expand All @@ -413,6 +451,7 @@ Future<Widget> bootstrap(
dataClient: userAppSettingsClient,
);
final userRepository = DataRepository<User>(dataClient: userClient);
logger.fine('All data repositories initialized.');

// Conditionally instantiate DemoDataMigrationService
final demoDataMigrationService =
Expand All @@ -422,6 +461,9 @@ Future<Widget> bootstrap(
userContentPreferencesRepository: userContentPreferencesRepository,
)
: null;
logger.fine(
'DemoDataMigrationService initialized: ${demoDataMigrationService != null}',
);

// Conditionally instantiate DemoDataInitializerService
// This service is responsible for ensuring that essential user-specific data
Expand All @@ -435,7 +477,11 @@ Future<Widget> bootstrap(
userContentPreferencesRepository: userContentPreferencesRepository,
)
: null;
logger.fine(
'DemoDataInitializerService initialized: ${demoDataInitializerService != null}',
);

logger.info('--- Bootstrap Process Complete. Returning App widget. ---');
return App(
authenticationRepository: authenticationRepository,
headlinesRepository: headlinesRepository,
Expand Down
36 changes: 36 additions & 0 deletions lib/router/go_router_observer.dart
Original file line number Diff line number Diff line change
@@ -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<dynamic> route, Route<dynamic>? previousRoute) {
logger.info(
'Pushed: ${route.settings.name} | from: ${previousRoute?.settings.name}',
);
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
logger.info(
'Popped: ${route.settings.name} | to: ${previousRoute?.settings.name}',
);
}

@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
logger.info(
'Removed: ${route.settings.name} | previous: ${previousRoute?.settings.name}',
);
}

@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
logger.info(
'Replaced: ${oldRoute?.settings.name} | with: ${newRoute?.settings.name}',
);
}
}
Loading
Loading