Skip to content

Commit d11418e

Browse files
authored
Merge pull request #110 from flutter-news-app-full-source-code/fix-bootstraping-dependency-chain
Fix bootstraping dependency chain
2 parents ac0424c + b130bea commit d11418e

20 files changed

+80
-63
lines changed

lib/ads/ad_navigator_observer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class AdNavigatorObserver extends NavigatorObserver {
118118
_pageTransitionCount >= requiredTransitions) {
119119
_logger.info('Interstitial ad due. Requesting ad.');
120120
_showInterstitialAd();
121-
_pageTransitionCount = 0; // Reset count after showing
121+
_pageTransitionCount = 0;
122122
}
123123
}
124124

lib/ads/admob_ad_provider.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AdMobAdProvider implements AdProvider {
2626
final Logger _logger;
2727
final Uuid _uuid = const Uuid();
2828

29-
static const _adLoadTimeout = 15; // Unified timeout for all ad types
29+
static const _adLoadTimeout = 15;
3030

3131
@override
3232
Future<void> initialize() async {

lib/ads/models/banner_ad.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:core/core.dart'; // Import core for AdPlatformType
1+
import 'package:core/core.dart';
22
import 'package:flutter/foundation.dart';
33
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/inline_ad.dart';
44

lib/ads/models/inline_ad.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:core/core.dart'; // Import core for AdPlatformType
1+
import 'package:core/core.dart';
22
import 'package:equatable/equatable.dart';
33
import 'package:flutter/foundation.dart';
44

lib/ads/models/interstitial_ad.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:core/core.dart'; // Import core for AdPlatformType
1+
import 'package:core/core.dart';
22
import 'package:equatable/equatable.dart';
33
import 'package:flutter/foundation.dart';
44

lib/ads/models/native_ad.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:core/core.dart'; // Import core for AdPlatformType
1+
import 'package:core/core.dart';
22
import 'package:flutter/foundation.dart';
33
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/inline_ad.dart';
44

lib/ads/widgets/admob_inline_ad_widget.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class AdmobInlineAdWidget extends StatefulWidget {
3838
}
3939

4040
class _AdmobInlineAdWidgetState extends State<AdmobInlineAdWidget> {
41-
admob.Ad? _ad; // Can be NativeAd or BannerAd
41+
admob.Ad? _ad;
4242
final Logger _logger = Logger('AdmobInlineAdWidget');
4343

4444
@override
@@ -80,7 +80,7 @@ class _AdmobInlineAdWidgetState extends State<AdmobInlineAdWidget> {
8080
} else if (widget.inlineAd.adObject is admob.BannerAd) {
8181
_ad = widget.inlineAd.adObject as admob.BannerAd;
8282
} else {
83-
_ad = null; // Ensure _ad is null if the type is incorrect
83+
_ad = null;
8484

8585
_logger.severe(
8686
'The provided ad object for AdMob inline ad is not of type '
@@ -111,7 +111,7 @@ class _AdmobInlineAdWidgetState extends State<AdmobInlineAdWidget> {
111111
// If largeThumbnail, assume mediumRectangle (300x250), otherwise standard banner (320x50).
112112
adHeight = widget.headlineImageStyle == HeadlineImageStyle.largeThumbnail
113113
? 250 // Height for mediumRectangle
114-
: 50; // Height for standard banner
114+
: 50;
115115
} else {
116116
// Fallback height for unknown inline ad types.
117117
adHeight = 100;

lib/ads/widgets/feed_ad_loader_widget.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
6262
InlineAd? _loadedAd;
6363
bool _isLoading = true;
6464
bool _hasError = false;
65-
final Logger _logger = Logger('FeedAdLoaderWidget'); // Renamed logger
65+
final Logger _logger = Logger('FeedAdLoaderWidget');
6666
final InlineAdCacheService _adCacheService = InlineAdCacheService();
6767

6868
/// Completer to manage the lifecycle of the ad loading future.
@@ -100,7 +100,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
100100
),
101101
);
102102
}
103-
_loadAdCompleter = null; // Clear the old completer for the new load
103+
_loadAdCompleter = null;
104104

105105
// Immediately set the widget to a loading state to prevent UI flicker.
106106
// This ensures a smooth transition from the old ad (or no ad) to the
@@ -110,7 +110,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
110110
_isLoading = true;
111111
_hasError = false;
112112
});
113-
_loadAd(); // Start loading the new ad
113+
_loadAd();
114114
}
115115
}
116116

@@ -160,7 +160,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
160160
// Complete the completer only if it hasn't been completed already
161161
// (e.g., by dispose() or didUpdateWidget() cancelling an old load).
162162
if (_loadAdCompleter?.isCompleted == false) {
163-
_loadAdCompleter!.complete(); // Complete the completer on success
163+
_loadAdCompleter!.complete();
164164
}
165165
return;
166166
}
@@ -220,7 +220,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
220220
});
221221
// Complete the completer only if it hasn't been completed already.
222222
if (_loadAdCompleter?.isCompleted == false) {
223-
_loadAdCompleter!.complete(); // Complete the completer on success
223+
_loadAdCompleter!.complete();
224224
}
225225
} else {
226226
_logger.warning(
@@ -237,7 +237,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
237237
if (_loadAdCompleter?.isCompleted == false) {
238238
_loadAdCompleter?.completeError(
239239
StateError('Failed to load ad: No ad returned.'),
240-
); // Complete with error
240+
);
241241
}
242242
}
243243
} catch (e, s) {
@@ -254,7 +254,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
254254
});
255255
// Complete the completer with an error only if it hasn't been completed already.
256256
if (_loadAdCompleter?.isCompleted == false) {
257-
_loadAdCompleter?.completeError(e); // Complete with error
257+
_loadAdCompleter?.completeError(e);
258258
}
259259
}
260260
}

lib/bootstrap.dart

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/services/dem
2121
import 'package:flutter_news_app_mobile_client_full_source_code/bloc_observer.dart';
2222
import 'package:flutter_news_app_mobile_client_full_source_code/shared/data/clients/country_inmemory_client.dart';
2323
import 'package:http_client/http_client.dart';
24+
import 'package:kv_storage_service/kv_storage_service.dart';
2425
import 'package:kv_storage_shared_preferences/kv_storage_shared_preferences.dart';
2526
import 'package:logging/logging.dart';
2627
import 'package:timeago/timeago.dart' as timeago;
@@ -32,22 +33,57 @@ Future<Widget> bootstrap(
3233
) async {
3334
WidgetsFlutterBinding.ensureInitialized();
3435
Bloc.observer = const AppBlocObserver();
35-
36+
final logger = Logger('bootstrap');
3637
timeago.setLocaleMessages('en', EnTimeagoMessages());
3738
timeago.setLocaleMessages('ar', ArTimeagoMessages());
3839

39-
final logger = Logger('bootstrap');
40-
40+
// 1. Initialize KV Storage Service first, as it's a foundational dependency.
4141
final kvStorage = await KVStorageSharedPreferences.getInstance();
4242

43+
// 2. Conditionally initialize HttpClient and Auth services based on environment.
44+
// This ensures HttpClient is available before any DataApi or AdProvider
45+
// that depends on it.
4346
late final AuthClient authClient;
4447
late final AuthRepository authenticationRepository;
45-
HttpClient? httpClient;
48+
late final HttpClient httpClient;
49+
if (appConfig.environment == app_config.AppEnvironment.demo) {
50+
// In-memory authentication for demo environment.
51+
authClient = AuthInmemory();
52+
authenticationRepository = AuthRepository(
53+
authClient: authClient,
54+
storageService: kvStorage,
55+
);
56+
// For demo, httpClient is not strictly needed for DataApi,
57+
// but we initialize a dummy one to satisfy non-nullable requirements
58+
// if any part of the code path expects it.
59+
// In a real scenario, DataApi would not be used in demo mode.
60+
httpClient = HttpClient(
61+
baseUrl: appConfig.baseUrl,
62+
tokenProvider: () async => null, // No token needed for demo
63+
logger: logger,
64+
);
65+
} else {
66+
// For production and development environments, an HTTP client is needed.
67+
// Initialize HttpClient first. Its tokenProvider now directly reads from
68+
// kvStorage, breaking the circular dependency with AuthRepository.
69+
httpClient = HttpClient(
70+
baseUrl: appConfig.baseUrl,
71+
tokenProvider: () =>
72+
kvStorage.readString(key: StorageKey.authToken.stringValue),
73+
logger: logger,
74+
);
4675

47-
// Initialize AdProvider and AdService
48-
// Initialize AdProvider based on platform.
49-
// On web, use a No-Op provider to prevent MissingPluginException,
50-
// as Google Mobile Ads SDK does not support native ads on web.
76+
// Now that httpClient is available, initialize AuthApi and AuthRepository.
77+
authClient = AuthApi(httpClient: httpClient);
78+
authenticationRepository = AuthRepository(
79+
authClient: authClient,
80+
storageService: kvStorage,
81+
);
82+
}
83+
84+
// 3. Initialize AdProvider and AdService.
85+
// These now have a guaranteed valid httpClient (for DataApi-based LocalAdProvider)
86+
// or can proceed independently (AdMobAdProvider).
5187
final adProviders = <AdPlatformType, AdProvider>{
5288
AdPlatformType.admob: AdMobAdProvider(logger: logger),
5389
AdPlatformType.local: LocalAdProvider(
@@ -60,7 +96,8 @@ Future<Widget> bootstrap(
6096
logger: logger,
6197
)
6298
: DataApi<LocalAd>(
63-
httpClient: httpClient!,
99+
httpClient:
100+
httpClient, // httpClient is now guaranteed to be non-null
64101
modelName: 'local_ad',
65102
fromJson: LocalAd.fromJson,
66103
toJson: LocalAd.toJson,
@@ -72,33 +109,14 @@ Future<Widget> bootstrap(
72109
};
73110

74111
final adService = AdService(adProviders: adProviders, logger: logger);
75-
await adService.initialize(); // Initialize all selected AdProviders early
76-
77-
if (appConfig.environment == app_config.AppEnvironment.demo) {
78-
authClient = AuthInmemory();
79-
authenticationRepository = AuthRepository(
80-
authClient: authClient,
81-
storageService: kvStorage,
82-
);
83-
} else {
84-
// For production and development environments, an HTTP client is needed.
85-
httpClient = HttpClient(
86-
baseUrl: appConfig.baseUrl,
87-
tokenProvider: () => authenticationRepository.getAuthToken(),
88-
logger: logger,
89-
);
90-
authClient = AuthApi(httpClient: httpClient);
91-
authenticationRepository = AuthRepository(
92-
authClient: authClient,
93-
storageService: kvStorage,
94-
);
95-
}
112+
await adService.initialize();
96113

97114
// Fetch the initial user from the authentication repository.
98115
// This ensures the AppBloc starts with an accurate authentication status.
99116
final initialUser = await authenticationRepository.getCurrentUser();
100117

101-
// Conditional data client instantiation based on environment
118+
// 4. Initialize all other DataClients and Repositories.
119+
// These now also have a guaranteed valid httpClient.
102120
DataClient<Headline> headlinesClient;
103121
DataClient<Topic> topicsClient;
104122
DataClient<Country> countriesClient;
@@ -107,8 +125,7 @@ Future<Widget> bootstrap(
107125
DataClient<UserAppSettings> userAppSettingsClient;
108126
DataClient<RemoteConfig> remoteConfigClient;
109127
DataClient<User> userClient;
110-
DataClient<LocalAd> localAdClient; // Declare localAdClient
111-
128+
DataClient<LocalAd> localAdClient;
112129
if (appConfig.environment == app_config.AppEnvironment.demo) {
113130
headlinesClient = DataInMemory<Headline>(
114131
toJson: (i) => i.toJson(),
@@ -192,7 +209,7 @@ Future<Widget> bootstrap(
192209
);
193210
} else if (appConfig.environment == app_config.AppEnvironment.development) {
194211
headlinesClient = DataApi<Headline>(
195-
httpClient: httpClient!,
212+
httpClient: httpClient,
196213
modelName: 'headline',
197214
fromJson: Headline.fromJson,
198215
toJson: (headline) => headline.toJson(),
@@ -257,7 +274,7 @@ Future<Widget> bootstrap(
257274
} else {
258275
// Default to API clients for production
259276
headlinesClient = DataApi<Headline>(
260-
httpClient: httpClient!,
277+
httpClient: httpClient,
261278
modelName: 'headline',
262279
fromJson: Headline.fromJson,
263280
toJson: (headline) => headline.toJson(),

lib/headlines-feed/bloc/countries_filter_bloc.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'package:core/core.dart';
66
import 'package:data_repository/data_repository.dart';
77
import 'package:equatable/equatable.dart';
88
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
9-
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/view/country_filter_page.dart'; // Import AppBloc
9+
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/view/country_filter_page.dart';
1010

1111
part 'countries_filter_event.dart';
1212
part 'countries_filter_state.dart';
@@ -38,7 +38,7 @@ class CountriesFilterBloc
3838
on<CountriesFilterApplyFollowedRequested>(
3939
_onCountriesFilterApplyFollowedRequested,
4040
transformer: restartable(),
41-
); // Register new event handler
41+
);
4242
}
4343

4444
final DataRepository<Country> _countriesRepository;

0 commit comments

Comments
 (0)