Skip to content

Commit 53a6e10

Browse files
authored
Merge pull request #118 from flutter-news-app-full-source-code/enhance-ads-feature
Enhance ads feature
2 parents 1141bf4 + bf04587 commit 53a6e10

29 files changed

+491
-194
lines changed

.vscode/launch.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "flutter-news-app-mobile-client-full-source-code",
9+
"request": "launch",
10+
"type": "dart"
11+
},
12+
{
13+
"name": "flutter-news-app-mobile-client-full-source-code (profile mode)",
14+
"request": "launch",
15+
"type": "dart",
16+
"flutterMode": "profile"
17+
},
18+
{
19+
"name": "flutter-news-app-mobile-client-full-source-code (release mode)",
20+
"request": "launch",
21+
"type": "dart",
22+
"flutterMode": "release"
23+
}
24+
]
25+
}

lib/account/bloc/account_bloc.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import 'package:bloc/bloc.dart';
55
import 'package:core/core.dart';
66
import 'package:data_repository/data_repository.dart';
77
import 'package:equatable/equatable.dart';
8-
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/config.dart'
9-
as local_config;
108
import 'package:logging/logging.dart';
119

1210
part 'account_event.dart';
@@ -17,11 +15,9 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
1715
required AuthRepository authenticationRepository,
1816
required DataRepository<UserContentPreferences>
1917
userContentPreferencesRepository,
20-
required local_config.AppEnvironment environment,
2118
Logger? logger,
2219
}) : _authenticationRepository = authenticationRepository,
2320
_userContentPreferencesRepository = userContentPreferencesRepository,
24-
_environment = environment,
2521
_logger = logger ?? Logger('AccountBloc'),
2622
super(const AccountState()) {
2723
// Listen to user changes from AuthRepository
@@ -57,7 +53,6 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
5753
final AuthRepository _authenticationRepository;
5854
final DataRepository<UserContentPreferences>
5955
_userContentPreferencesRepository;
60-
final local_config.AppEnvironment _environment;
6156
final Logger _logger;
6257
late StreamSubscription<User?> _userSubscription;
6358
late StreamSubscription<Type> _userContentPreferencesSubscription;

lib/ads/ad_provider.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,9 @@ abstract class AdProvider {
7171
required String? adId,
7272
required AdThemeStyle adThemeStyle,
7373
});
74+
75+
/// This method is responsible for releasing the resources held by the
76+
/// platform-specific ad object (e.g., `google_mobile_ads.Ad`).
77+
/// It should be called when an ad is no longer needed to prevent memory leaks.
78+
Future<void> disposeAd(Object adObject);
7479
}

lib/ads/ad_service.dart

Lines changed: 128 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.
33
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/inline_ad.dart';
55
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/interstitial_ad.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_environment.dart';
67
import 'package:logging/logging.dart';
78

89
/// {@template ad_service}
@@ -20,13 +21,21 @@ class AdService {
2021
/// These providers will be used to load ads from specific ad networks.
2122
AdService({
2223
required Map<AdPlatformType, AdProvider> adProviders,
24+
required AppEnvironment environment,
2325
Logger? logger,
2426
}) : _adProviders = adProviders,
27+
_environment = environment,
2528
_logger = logger ?? Logger('AdService');
2629

2730
final Map<AdPlatformType, AdProvider> _adProviders;
31+
final AppEnvironment _environment;
2832
final Logger _logger;
2933

34+
// Configurable retry parameters for ad loading.
35+
// TODO(fulleni): Make this configurable through the remote config.
36+
static const int _maxAdLoadRetries = 2;
37+
static const Duration _adLoadRetryDelay = Duration(seconds: 1);
38+
3039
/// Initializes all underlying ad providers.
3140
///
3241
/// This should be called once at application startup to ensure all
@@ -39,6 +48,41 @@ class AdService {
3948
_logger.info('AdService: AdService initialized.');
4049
}
4150

51+
/// Disposes of an ad object by delegating to the appropriate [AdProvider].
52+
///
53+
/// This method is called by the [InlineAdCacheService] to ensure that
54+
/// inline ad resources are released when an ad is removed from the cache
55+
/// or replaced. It also handles disposal of interstitial ads.
56+
Future<void> disposeAd(dynamic adModel) async {
57+
// Determine the AdPlatformType from the adModel if it's an InlineAd or InterstitialAd.
58+
AdPlatformType? providerType;
59+
Object? adObject; // To hold the actual ad object
60+
61+
if (adModel is InlineAd) {
62+
providerType = adModel.provider;
63+
adObject = adModel.adObject;
64+
} else if (adModel is InterstitialAd) {
65+
providerType = adModel.provider;
66+
adObject = adModel.adObject;
67+
}
68+
69+
if (providerType != null && adObject != null) {
70+
final adProvider = _adProviders[providerType];
71+
if (adProvider != null) {
72+
await adProvider.disposeAd(adObject); // Pass the actual ad object
73+
} else {
74+
_logger.warning(
75+
'AdService: No AdProvider found for type $providerType to dispose ad.',
76+
);
77+
}
78+
} else {
79+
_logger.warning(
80+
'AdService: Cannot determine AdPlatformType or ad object for ad model of type '
81+
'${adModel.runtimeType}. Cannot dispose ad.',
82+
);
83+
}
84+
}
85+
4286
/// Retrieves a loaded inline ad (native or banner) for display in a feed.
4387
///
4488
/// This method delegates the ad loading to the appropriate [AdProvider]
@@ -95,6 +139,18 @@ class AdService {
95139
}
96140

97141
final primaryAdPlatform = adConfig.primaryAdPlatform;
142+
143+
// If RemoteConfig specifies AdPlatformType.demo but the app is not in demo environment,
144+
// log a warning and skip ad load.
145+
if (primaryAdPlatform == AdPlatformType.demo &&
146+
_environment != AppEnvironment.demo) {
147+
_logger.warning(
148+
'AdService: RemoteConfig specifies AdPlatformType.demo as primary '
149+
'ad platform, but app is not in demo environment. Skipping interstitial ad load.',
150+
);
151+
return null;
152+
}
153+
98154
final adProvider = _adProviders[primaryAdPlatform];
99155

100156
if (adProvider == null) {
@@ -224,6 +280,18 @@ class AdService {
224280
}
225281

226282
final primaryAdPlatform = adConfig.primaryAdPlatform;
283+
284+
// If RemoteConfig specifies AdPlatformType.demo but the app is not in demo environment,
285+
// log a warning and skip ad load.
286+
if (primaryAdPlatform == AdPlatformType.demo &&
287+
_environment != AppEnvironment.demo) {
288+
_logger.warning(
289+
'AdService: RemoteConfig specifies AdPlatformType.demo as primary '
290+
'ad platform, but app is not in demo environment. Skipping inline ad load.',
291+
);
292+
return null;
293+
}
294+
227295
final adProvider = _adProviders[primaryAdPlatform];
228296

229297
if (adProvider == null) {
@@ -258,53 +326,69 @@ class AdService {
258326
return null;
259327
}
260328

261-
_logger.info(
262-
'AdService: Requesting $adType ad from $primaryAdPlatform AdProvider with ID: $adId '
263-
'for ${feedAd ? 'feed' : 'in-article'} placement.',
264-
);
265-
try {
266-
InlineAd? loadedAd;
267-
// For in-article banner ads, bannerAdShape dictates the visual style.
268-
// For feed ads, headlineImageStyle is still relevant.
269-
final effectiveHeadlineImageStyle = feedAd ? headlineImageStyle : null;
270-
271-
switch (adType) {
272-
case AdType.native:
273-
loadedAd = await adProvider.loadNativeAd(
274-
adPlatformIdentifiers: platformAdIdentifiers,
275-
adId: adId,
276-
adThemeStyle: adThemeStyle,
277-
headlineImageStyle: effectiveHeadlineImageStyle,
278-
);
279-
case AdType.banner:
280-
loadedAd = await adProvider.loadBannerAd(
281-
adPlatformIdentifiers: platformAdIdentifiers,
282-
adId: adId,
283-
adThemeStyle: adThemeStyle,
284-
headlineImageStyle: effectiveHeadlineImageStyle,
285-
);
286-
case AdType.interstitial:
287-
case AdType.video:
288-
_logger.warning(
289-
'AdService: Attempted to load $adType ad using _loadInlineAd. This is not supported.',
290-
);
291-
return null;
329+
for (var attempt = 0; attempt <= _maxAdLoadRetries; attempt++) {
330+
if (attempt > 0) {
331+
_logger.info(
332+
'AdService: Retrying $adType ad load (attempt $attempt) for ID: $adId '
333+
'after $_adLoadRetryDelay delay.',
334+
);
335+
await Future<void>.delayed(_adLoadRetryDelay);
292336
}
293337

294-
if (loadedAd != null) {
295-
_logger.info('AdService: $adType ad successfully loaded.');
296-
return loadedAd;
297-
} else {
298-
_logger.info('AdService: No $adType ad loaded by AdProvider.');
299-
return null;
338+
try {
339+
_logger.info(
340+
'AdService: Requesting $adType ad from $primaryAdPlatform AdProvider with ID: $adId '
341+
'for ${feedAd ? 'feed' : 'in-article'} placement.',
342+
);
343+
InlineAd? loadedAd;
344+
// For in-article banner ads, bannerAdShape dictates the visual style.
345+
// For feed ads, headlineImageStyle is still relevant.
346+
final effectiveHeadlineImageStyle = feedAd ? headlineImageStyle : null;
347+
348+
switch (adType) {
349+
case AdType.native:
350+
loadedAd = await adProvider.loadNativeAd(
351+
adPlatformIdentifiers: platformAdIdentifiers,
352+
adId: adId,
353+
adThemeStyle: adThemeStyle,
354+
headlineImageStyle: effectiveHeadlineImageStyle,
355+
);
356+
case AdType.banner:
357+
loadedAd = await adProvider.loadBannerAd(
358+
adPlatformIdentifiers: platformAdIdentifiers,
359+
adId: adId,
360+
adThemeStyle: adThemeStyle,
361+
headlineImageStyle: effectiveHeadlineImageStyle,
362+
);
363+
case AdType.interstitial:
364+
case AdType.video:
365+
_logger.warning(
366+
'AdService: Attempted to load $adType ad using _loadInlineAd. This is not supported.',
367+
);
368+
return null;
369+
}
370+
371+
if (loadedAd != null) {
372+
_logger.info('AdService: $adType ad successfully loaded.');
373+
return loadedAd;
374+
} else {
375+
_logger.info('AdService: No $adType ad loaded by AdProvider.');
376+
// If no ad is returned, it might be a "no fill" scenario.
377+
// Continue to the next retry attempt.
378+
}
379+
} catch (e, s) {
380+
_logger.severe(
381+
'AdService: Error getting $adType ad from AdProvider on attempt $attempt: $e',
382+
e,
383+
s,
384+
);
385+
// If an exception occurs, log it and continue to the next retry attempt.
300386
}
301-
} catch (e, s) {
302-
_logger.severe(
303-
'AdService: Error getting $adType ad from AdProvider: $e',
304-
e,
305-
s,
306-
);
307-
return null;
308387
}
388+
389+
_logger.warning(
390+
'AdService: All retry attempts failed for $adType ad with ID: $adId.',
391+
);
392+
return null;
309393
}
310394
}

lib/ads/admob_ad_provider.dart

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class AdMobAdProvider implements AdProvider {
8888
},
8989
onAdFailedToLoad: (ad, error) {
9090
_logger.severe('AdMobAdProvider: Native Ad failed to load: $error');
91+
// Ad disposal is now handled by InlineAdCacheService
9192
completer.complete(null);
9293
},
9394
onAdClicked: (ad) {
@@ -119,7 +120,8 @@ class AdMobAdProvider implements AdProvider {
119120
const Duration(seconds: _adLoadTimeout),
120121
onTimeout: () {
121122
_logger.warning('AdMobAdProvider: Native ad loading timed out.');
122-
ad.dispose();
123+
// Ad disposal is now handled by InlineAdCacheService
124+
completer.complete(null);
123125
return null;
124126
},
125127
);
@@ -177,7 +179,7 @@ class AdMobAdProvider implements AdProvider {
177179
},
178180
onAdFailedToLoad: (ad, error) {
179181
_logger.severe('AdMobAdProvider: Banner Ad failed to load: $error');
180-
ad.dispose();
182+
// Ad disposal is now handled by InlineAdCacheService
181183
completer.complete(null);
182184
},
183185
onAdOpened: (ad) {
@@ -203,7 +205,8 @@ class AdMobAdProvider implements AdProvider {
203205
const Duration(seconds: _adLoadTimeout),
204206
onTimeout: () {
205207
_logger.warning('AdMobAdProvider: Banner ad loading timed out.');
206-
ad.dispose();
208+
// Ad disposal is now handled by InlineAdCacheService
209+
completer.complete(null);
207210
return null;
208211
},
209212
);
@@ -327,4 +330,20 @@ class AdMobAdProvider implements AdProvider {
327330
),
328331
);
329332
}
333+
334+
@override
335+
Future<void> disposeAd(Object adObject) async {
336+
_logger.info('AdMobAdProvider: Attempting to dispose ad object: $adObject');
337+
if (adObject is admob.Ad) {
338+
await adObject.dispose();
339+
_logger.info(
340+
'AdMobAdProvider: Disposed AdMob ad object (NativeAd, BannerAd, or InterstitialAd).',
341+
);
342+
} else {
343+
_logger.warning(
344+
'AdMobAdProvider: Attempted to dispose a non-AdMob ad object. '
345+
'Type: ${adObject.runtimeType}',
346+
);
347+
}
348+
}
330349
}

lib/ads/demo_ad_provider.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,14 @@ class DemoAdProvider implements AdProvider {
8585
adObject: Object(), // Placeholder object
8686
);
8787
}
88+
89+
@override
90+
Future<void> disposeAd(Object adObject) async {
91+
_logger.info(
92+
'DemoAdProvider: No explicit disposal needed for demo ad object: '
93+
'${adObject.runtimeType}',
94+
);
95+
// Demo ad objects are simple Dart objects and do not hold native resources
96+
// that require explicit disposal.
97+
}
8898
}

0 commit comments

Comments
 (0)