Skip to content

Commit 72ffebb

Browse files
authored
Merge pull request #80 from flutter-news-app-full-source-code/Implementing-an-Extensible-Ad-Network-Service
Implementing an extensible ad network service
2 parents a47702e + 1fba26f commit 72ffebb

File tree

13 files changed

+407
-61
lines changed

13 files changed

+407
-61
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ Easily switch between development (in-memory data or local API) and production e
7373
Fully internationalized with working English and Arabic localizations (`.arb` files). Adding more languages is straightforward.
7474
> **Your Advantage:** Easily adapt your application for a global audience and tap into new markets. 🌐
7575
76+
#### 💰 **Extensible Monetization with In-Feed Ads**
77+
* Built with a clean, provider-based ad architecture that makes monetization simple and flexible.
78+
* Comes with a ready-to-go Google AdMob integration for native ads in the feed.
79+
* The decoupled design means you can easily swap in or add other ad networks (like Meta Audience Network) in the future without rewriting your core logic.
80+
> **Your Advantage:** Start generating revenue from day one with a robust ad system that’s built to scale with your business. 💸
81+
7682
---
7783

7884
## 🔑 License: Source-Available with a Free Trial

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
<meta-data
3939
android:name="flutterEmbedding"
4040
android:value="2" />
41+
42+
<!-- TODO(fulleni): Replace with your real AdMob App ID -->
43+
<meta-data
44+
android:name="com.google.android.gms.ads.APPLICATION_ID"
45+
android:value="ca-app-pub-3940256099942544~3347511713"/>
4146
</application>
4247
<!-- Required to query activities that can process text, see:
4348
https://developer.android.com/training/package-visibility and

lib/ads/ad_provider.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:google_mobile_ads/google_mobile_ads.dart';
2+
3+
/// {@template ad_provider}
4+
/// An abstract class defining the interface for any ad network provider.
5+
///
6+
/// This abstraction allows the application to integrate with different
7+
/// ad networks (e.g., AdMob, Meta Audience Network) through a common API,
8+
/// promoting extensibility and decoupling.
9+
/// {@endtemplate}
10+
abstract class AdProvider {
11+
/// {@macro ad_provider}
12+
const AdProvider();
13+
14+
/// Initializes the ad network SDK.
15+
///
16+
/// This method should be called once at application startup.
17+
/// It handles any necessary setup for the specific ad network.
18+
Future<void> initialize();
19+
20+
/// Loads a native ad.
21+
///
22+
/// Returns a [NativeAd] object if an ad is successfully loaded,
23+
/// otherwise returns `null`.
24+
Future<NativeAd?> loadNativeAd();
25+
26+
// Future methods for other ad types (e.g., interstitial, banner)
27+
// can be added here as needed in the future.
28+
}

lib/ads/ad_service.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
2+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_feed_item.dart';
3+
import 'package:logging/logging.dart';
4+
import 'package:uuid/uuid.dart';
5+
6+
/// {@template ad_service}
7+
/// A service responsible for managing and providing ads to the application.
8+
///
9+
/// This service acts as an intermediary between the application's UI/logic
10+
/// and the underlying ad network providers (e.g., AdMob). It handles
11+
/// requesting ads and wrapping them in a generic [AdFeedItem] for use
12+
/// in the feed.
13+
/// {@endtemplate}
14+
class AdService {
15+
/// {@macro ad_service}
16+
///
17+
/// Requires an [AdProvider] to be injected, which will be used to
18+
/// load ads from a specific ad network.
19+
AdService({required AdProvider adProvider, Logger? logger})
20+
: _adProvider = adProvider,
21+
_logger = logger ?? Logger('AdService');
22+
23+
final AdProvider _adProvider;
24+
final Logger _logger;
25+
final Uuid _uuid = const Uuid();
26+
27+
/// Initializes the underlying ad provider.
28+
///
29+
/// This should be called once at application startup.
30+
Future<void> initialize() async {
31+
_logger.info('Initializing AdService...');
32+
await _adProvider.initialize();
33+
_logger.info('AdService initialized.');
34+
}
35+
36+
/// Retrieves a loaded native ad wrapped as an [AdFeedItem].
37+
///
38+
/// This method delegates the ad loading to the injected [AdProvider].
39+
/// If an ad is successfully loaded, it's wrapped in an [AdFeedItem]
40+
/// with a unique ID.
41+
///
42+
/// Returns an [AdFeedItem] if an ad is available, otherwise `null`.
43+
Future<AdFeedItem?> getAd() async {
44+
_logger.info('Requesting native ad from AdProvider...');
45+
try {
46+
final nativeAd = await _adProvider.loadNativeAd();
47+
if (nativeAd != null) {
48+
_logger.info('Native ad successfully loaded and wrapped.');
49+
return AdFeedItem(id: _uuid.v4(), nativeAd: nativeAd);
50+
} else {
51+
_logger.info('No native ad loaded by AdProvider.');
52+
return null;
53+
}
54+
} catch (e) {
55+
_logger.severe('Error getting ad from AdProvider: $e');
56+
return null;
57+
}
58+
}
59+
}

lib/ads/admob_ad_provider.dart

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
5+
import 'package:google_mobile_ads/google_mobile_ads.dart';
6+
import 'package:logging/logging.dart';
7+
8+
/// {@template admob_ad_provider}
9+
/// A concrete implementation of [AdProvider] for Google AdMob.
10+
///
11+
/// This class handles the initialization of the Google Mobile Ads SDK
12+
/// and the loading of native ads specifically for AdMob.
13+
/// {@endtemplate}
14+
class AdMobAdProvider implements AdProvider {
15+
/// {@macro admob_ad_provider}
16+
AdMobAdProvider({Logger? logger}) : _logger = logger ?? Logger('AdMobAdProvider');
17+
18+
final Logger _logger;
19+
20+
/// The AdMob Native Ad Unit ID for Android.
21+
///
22+
/// This should be replaced with your production Ad Unit ID.
23+
/// For testing, use `ca-app-pub-3940256099942544/2247696110`.
24+
static const String _androidNativeAdUnitId =
25+
'ca-app-pub-3940256099942544/2247696110';
26+
27+
/// The AdMob Native Ad Unit ID for iOS.
28+
///
29+
/// This should be replaced with your production Ad Unit ID.
30+
/// For testing, use `ca-app-pub-3940256099942544/3986624511`.
31+
static const String _iosNativeAdUnitId =
32+
'ca-app-pub-3940256099942544/3986624511';
33+
34+
/// The AdMob Native Ad Unit ID for Web.
35+
///
36+
/// AdMob does not officially support native ads on web. This is a placeholder.
37+
/// For testing, use `ca-app-pub-3940256099942544/2247696110` (Android test ID).
38+
static const String _webNativeAdUnitId =
39+
'ca-app-pub-3940256099942544/2247696110';
40+
41+
/// Returns the appropriate native ad unit ID based on the platform.
42+
String get _nativeAdUnitId {
43+
if (kIsWeb) {
44+
return _webNativeAdUnitId;
45+
} else if (defaultTargetPlatform == TargetPlatform.android) {
46+
return _androidNativeAdUnitId;
47+
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
48+
return _iosNativeAdUnitId;
49+
}
50+
return ''; // Fallback for unsupported platforms
51+
}
52+
53+
@override
54+
Future<void> initialize() async {
55+
_logger.info('Initializing Google Mobile Ads SDK...');
56+
try {
57+
await MobileAds.instance.initialize();
58+
_logger.info('Google Mobile Ads SDK initialized successfully.');
59+
} catch (e) {
60+
_logger.severe('Failed to initialize Google Mobile Ads SDK: $e');
61+
// TODO(fulleni): Depending on requirements, you might want to rethrow or handle this more gracefully.
62+
// For now, we log and continue, as ad loading might still work in some cases.
63+
}
64+
}
65+
66+
@override
67+
Future<NativeAd?> loadNativeAd() async {
68+
if (_nativeAdUnitId.isEmpty) {
69+
_logger.warning('No native ad unit ID configured for this platform.');
70+
return null;
71+
}
72+
73+
_logger.info('Attempting to load native ad from unit ID: $_nativeAdUnitId');
74+
75+
final completer = Completer<NativeAd?>();
76+
77+
final ad = NativeAd(
78+
adUnitId: _nativeAdUnitId,
79+
factoryId: 'listTile', // This ID must match a factory in your native code
80+
request: const AdRequest(),
81+
listener: NativeAdListener(
82+
onAdLoaded: (ad) {
83+
_logger.info('Native Ad loaded successfully.');
84+
completer.complete(ad as NativeAd);
85+
},
86+
onAdFailedToLoad: (ad, error) {
87+
_logger.severe('Native Ad failed to load: $error');
88+
ad.dispose();
89+
completer.complete(null);
90+
},
91+
onAdClicked: (ad) {
92+
_logger.info('Native Ad clicked.');
93+
},
94+
onAdImpression: (ad) {
95+
_logger.info('Native Ad impression recorded.');
96+
},
97+
onAdClosed: (ad) {
98+
_logger.info('Native Ad closed.');
99+
ad.dispose();
100+
},
101+
onAdOpened: (ad) {
102+
_logger.info('Native Ad opened.');
103+
},
104+
onAdWillDismissScreen: (ad) {
105+
_logger.info('Native Ad will dismiss screen.');
106+
},
107+
),
108+
);
109+
110+
try {
111+
await ad.load();
112+
} catch (e) {
113+
_logger.severe('Error during native ad load: $e');
114+
completer.complete(null);
115+
}
116+
117+
return completer.future;
118+
}
119+
}

lib/ads/models/ad_feed_item.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'package:core/core.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:google_mobile_ads/google_mobile_ads.dart';
4+
5+
/// {@template ad_feed_item}
6+
/// A [FeedItem] that wraps a loaded native ad object from an ad network SDK.
7+
///
8+
/// This class allows actual, displayable ad objects (like [NativeAd] from
9+
/// Google Mobile Ads) to be seamlessly integrated into the application's
10+
/// generic feed structure alongside other content types (e.g., [Headline]).
11+
/// {@endtemplate}
12+
class AdFeedItem extends FeedItem with EquatableMixin {
13+
/// {@macro ad_feed_item}
14+
const AdFeedItem({
15+
required this.id,
16+
required this.nativeAd,
17+
}) : super(type: 'ad_feed_item');
18+
19+
/// A unique identifier for this specific ad instance in the feed.
20+
///
21+
/// This is distinct from the ad unit ID and is used for tracking
22+
/// the ad within the feed.
23+
final String id;
24+
25+
/// The loaded native ad object from the ad network SDK.
26+
///
27+
/// This object contains the actual ad content and is ready for display.
28+
final NativeAd nativeAd;
29+
30+
@override
31+
List<Object?> get props => [id, nativeAd, type];
32+
}

lib/app/view/app.dart

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:data_repository/data_repository.dart';
44
import 'package:flex_color_scheme/flex_color_scheme.dart';
55
import 'package:flutter/material.dart';
66
import 'package:flutter_bloc/flutter_bloc.dart';
7+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart';
78
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
89
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_environment.dart';
910
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/app_status_service.dart';
@@ -25,24 +26,26 @@ class App extends StatelessWidget {
2526
required DataRepository<Source> sourcesRepository,
2627
required DataRepository<UserAppSettings> userAppSettingsRepository,
2728
required DataRepository<UserContentPreferences>
28-
userContentPreferencesRepository,
29+
userContentPreferencesRepository,
2930
required DataRepository<RemoteConfig> remoteConfigRepository,
3031
required DataRepository<User> userRepository,
3132
required KVStorageService kvStorageService,
3233
required AppEnvironment environment,
34+
required AdService adService,
3335
this.demoDataMigrationService,
3436
super.key,
35-
}) : _authenticationRepository = authenticationRepository,
36-
_headlinesRepository = headlinesRepository,
37-
_topicsRepository = topicsRepository,
38-
_countriesRepository = countriesRepository,
39-
_sourcesRepository = sourcesRepository,
40-
_userAppSettingsRepository = userAppSettingsRepository,
41-
_userContentPreferencesRepository = userContentPreferencesRepository,
42-
_appConfigRepository = remoteConfigRepository,
43-
_userRepository = userRepository,
44-
_kvStorageService = kvStorageService,
45-
_environment = environment;
37+
}) : _authenticationRepository = authenticationRepository,
38+
_headlinesRepository = headlinesRepository,
39+
_topicsRepository = topicsRepository,
40+
_countriesRepository = countriesRepository,
41+
_sourcesRepository = sourcesRepository,
42+
_userAppSettingsRepository = userAppSettingsRepository,
43+
_userContentPreferencesRepository = userContentPreferencesRepository,
44+
_appConfigRepository = remoteConfigRepository,
45+
_userRepository = userRepository,
46+
_kvStorageService = kvStorageService,
47+
_environment = environment,
48+
_adService = adService;
4649

4750
final AuthRepository _authenticationRepository;
4851
final DataRepository<Headline> _headlinesRepository;
@@ -51,11 +54,12 @@ class App extends StatelessWidget {
5154
final DataRepository<Source> _sourcesRepository;
5255
final DataRepository<UserAppSettings> _userAppSettingsRepository;
5356
final DataRepository<UserContentPreferences>
54-
_userContentPreferencesRepository;
57+
_userContentPreferencesRepository;
5558
final DataRepository<RemoteConfig> _appConfigRepository;
5659
final DataRepository<User> _userRepository;
5760
final KVStorageService _kvStorageService;
5861
final AppEnvironment _environment;
62+
final AdService _adService;
5963
final DemoDataMigrationService? demoDataMigrationService;
6064

6165
@override
@@ -72,15 +76,17 @@ class App extends StatelessWidget {
7276
RepositoryProvider.value(value: _appConfigRepository),
7377
RepositoryProvider.value(value: _userRepository),
7478
RepositoryProvider.value(value: _kvStorageService),
79+
RepositoryProvider.value(value: _adService),
7580
],
7681
child: MultiBlocProvider(
7782
providers: [
7883
BlocProvider(
7984
create: (context) => AppBloc(
8085
authenticationRepository: context.read<AuthRepository>(),
81-
userAppSettingsRepository: context
82-
.read<DataRepository<UserAppSettings>>(),
83-
appConfigRepository: context.read<DataRepository<RemoteConfig>>(),
86+
userAppSettingsRepository:
87+
context.read<DataRepository<UserAppSettings>>(),
88+
appConfigRepository:
89+
context.read<DataRepository<RemoteConfig>>(),
8490
userRepository: context.read<DataRepository<User>>(),
8591
environment: _environment,
8692
demoDataMigrationService: demoDataMigrationService,
@@ -103,6 +109,7 @@ class App extends StatelessWidget {
103109
appConfigRepository: _appConfigRepository,
104110
userRepository: _userRepository,
105111
environment: _environment,
112+
adService: _adService,
106113
),
107114
),
108115
);
@@ -121,6 +128,7 @@ class _AppView extends StatefulWidget {
121128
required this.appConfigRepository,
122129
required this.userRepository,
123130
required this.environment,
131+
required this.adService,
124132
});
125133

126134
final AuthRepository authenticationRepository;
@@ -133,6 +141,7 @@ class _AppView extends StatefulWidget {
133141
final DataRepository<RemoteConfig> appConfigRepository;
134142
final DataRepository<User> userRepository;
135143
final AppEnvironment environment;
144+
final AdService adService;
136145

137146
@override
138147
State<_AppView> createState() => _AppViewState();
@@ -174,6 +183,7 @@ class _AppViewState extends State<_AppView> {
174183
remoteConfigRepository: widget.appConfigRepository,
175184
userRepository: widget.userRepository,
176185
environment: widget.environment,
186+
adService: widget.adService,
177187
);
178188

179189
// Removed Dynamic Link Initialization

0 commit comments

Comments
 (0)