Skip to content

Commit faead38

Browse files
authored
Merge pull request #81 from flutter-news-app-full-source-code/Decoupling-the-Ad-Subsystem
Decoupling the ad subsystem
2 parents 72ffebb + 7f95e35 commit faead38

24 files changed

+464
-100
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ This app comes packed with all the features you need to launch a professional ne
2929
* Includes a dedicated search page to help users find specific content quickly.
3030
> **Your Advantage:** Give your users powerful content discovery tools that keep them engaged and coming back for more. 🎯
3131
32+
#### 💰 **Extensible Monetization with In-Feed Ads**
33+
* Built with a clean, provider-agnostic ad architecture that makes monetization simple and flexible.
34+
* Features a decoupled design, allowing easy integration and swapping of various ad networks (e.g., Google AdMob, Meta Audience Network) without rewriting core logic.
35+
* Comes with a ready-to-go Google AdMob integration for native ads in the feed.
36+
> **Your Advantage:** Start generating revenue from day one with a highly extensible and robust ad system that’s built to scale with your business. 💸
37+
3238
#### 🔐 **Robust User Authentication**
3339
Secure and flexible authentication flows are built-in:
3440
* 📧 **Email + Code (Passwordless) Sign-In:** Modern and secure.
@@ -73,12 +79,6 @@ Easily switch between development (in-memory data or local API) and production e
7379
Fully internationalized with working English and Arabic localizations (`.arb` files). Adding more languages is straightforward.
7480
> **Your Advantage:** Easily adapt your application for a global audience and tap into new markets. 🌐
7581
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-
8282
---
8383

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

lib/ads/ad_provider.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import 'package:google_mobile_ads/google_mobile_ads.dart';
1+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
2+
as app_native_ad;
23

34
/// {@template ad_provider}
45
/// An abstract class defining the interface for any ad network provider.
@@ -19,9 +20,9 @@ abstract class AdProvider {
1920

2021
/// Loads a native ad.
2122
///
22-
/// Returns a [NativeAd] object if an ad is successfully loaded,
23+
/// Returns a [app_native_ad.NativeAd] object if an ad is successfully loaded,
2324
/// otherwise returns `null`.
24-
Future<NativeAd?> loadNativeAd();
25+
Future<app_native_ad.NativeAd?> loadNativeAd();
2526

2627
// Future methods for other ad types (e.g., interstitial, banner)
2728
// can be added here as needed in the future.

lib/ads/ad_service.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class AdService {
1717
/// Requires an [AdProvider] to be injected, which will be used to
1818
/// load ads from a specific ad network.
1919
AdService({required AdProvider adProvider, Logger? logger})
20-
: _adProvider = adProvider,
21-
_logger = logger ?? Logger('AdService');
20+
: _adProvider = adProvider,
21+
_logger = logger ?? Logger('AdService');
2222

2323
final AdProvider _adProvider;
2424
final Logger _logger;

lib/ads/admob_ad_provider.dart

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,27 @@ import 'dart:async';
22

33
import 'package:flutter/foundation.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
5-
import 'package:google_mobile_ads/google_mobile_ads.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
6+
as app_native_ad;
7+
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
68
import 'package:logging/logging.dart';
9+
import 'package:uuid/uuid.dart';
710

811
/// {@template admob_ad_provider}
912
/// A concrete implementation of [AdProvider] for Google AdMob.
1013
///
1114
/// This class handles the initialization of the Google Mobile Ads SDK
12-
/// and the loading of native ads specifically for AdMob.
15+
/// and the loading of native ads specifically for AdMob. It adapts the
16+
/// AdMob-specific [admob.NativeAd] object into our generic [app_native_ad.NativeAd]
17+
/// model.
1318
/// {@endtemplate}
1419
class AdMobAdProvider implements AdProvider {
1520
/// {@macro admob_ad_provider}
16-
AdMobAdProvider({Logger? logger}) : _logger = logger ?? Logger('AdMobAdProvider');
21+
AdMobAdProvider({Logger? logger})
22+
: _logger = logger ?? Logger('AdMobAdProvider');
1723

1824
final Logger _logger;
25+
final Uuid _uuid = const Uuid();
1926

2027
/// The AdMob Native Ad Unit ID for Android.
2128
///
@@ -54,7 +61,7 @@ class AdMobAdProvider implements AdProvider {
5461
Future<void> initialize() async {
5562
_logger.info('Initializing Google Mobile Ads SDK...');
5663
try {
57-
await MobileAds.instance.initialize();
64+
await admob.MobileAds.instance.initialize();
5865
_logger.info('Google Mobile Ads SDK initialized successfully.');
5966
} catch (e) {
6067
_logger.severe('Failed to initialize Google Mobile Ads SDK: $e');
@@ -64,24 +71,24 @@ class AdMobAdProvider implements AdProvider {
6471
}
6572

6673
@override
67-
Future<NativeAd?> loadNativeAd() async {
74+
Future<app_native_ad.NativeAd?> loadNativeAd() async {
6875
if (_nativeAdUnitId.isEmpty) {
6976
_logger.warning('No native ad unit ID configured for this platform.');
7077
return null;
7178
}
7279

7380
_logger.info('Attempting to load native ad from unit ID: $_nativeAdUnitId');
7481

75-
final completer = Completer<NativeAd?>();
82+
final completer = Completer<admob.NativeAd?>();
7683

77-
final ad = NativeAd(
84+
final ad = admob.NativeAd(
7885
adUnitId: _nativeAdUnitId,
7986
factoryId: 'listTile', // This ID must match a factory in your native code
80-
request: const AdRequest(),
81-
listener: NativeAdListener(
87+
request: const admob.AdRequest(),
88+
listener: admob.NativeAdListener(
8289
onAdLoaded: (ad) {
8390
_logger.info('Native Ad loaded successfully.');
84-
completer.complete(ad as NativeAd);
91+
completer.complete(ad as admob.NativeAd);
8592
},
8693
onAdFailedToLoad: (ad, error) {
8794
_logger.severe('Native Ad failed to load: $error');
@@ -114,6 +121,25 @@ class AdMobAdProvider implements AdProvider {
114121
completer.complete(null);
115122
}
116123

117-
return completer.future;
124+
// Add a timeout to the future to prevent hanging if callbacks are not called.
125+
final googleNativeAd = await completer.future.timeout(
126+
const Duration(seconds: 15),
127+
onTimeout: () {
128+
_logger.warning('Native ad loading timed out.');
129+
ad.dispose(); // Dispose the ad if it timed out
130+
return null;
131+
},
132+
);
133+
134+
if (googleNativeAd == null) {
135+
return null;
136+
}
137+
138+
// Map the Google Mobile Ads NativeAd to our generic NativeAd model.
139+
// Only the ID and the raw adObject are stored, as per the simplified model.
140+
return app_native_ad.NativeAd(
141+
id: _uuid.v4(), // Generate a unique ID for our internal model
142+
adObject: googleNativeAd, // Store the original AdMob object
143+
);
118144
}
119145
}

lib/ads/models/ad_feed_item.dart

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
import 'package:core/core.dart';
22
import 'package:equatable/equatable.dart';
3-
import 'package:google_mobile_ads/google_mobile_ads.dart';
3+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
4+
as app_native_ad;
45

56
/// {@template ad_feed_item}
67
/// A [FeedItem] that wraps a loaded native ad object from an ad network SDK.
78
///
8-
/// This class allows actual, displayable ad objects (like [NativeAd] from
9-
/// Google Mobile Ads) to be seamlessly integrated into the application's
9+
/// This class allows actual, displayable ad objects (like [app_native_ad.NativeAd]
10+
/// from our generic ad model) to be seamlessly integrated into the application's
1011
/// generic feed structure alongside other content types (e.g., [Headline]).
1112
/// {@endtemplate}
1213
class AdFeedItem extends FeedItem with EquatableMixin {
1314
/// {@macro ad_feed_item}
14-
const AdFeedItem({
15-
required this.id,
16-
required this.nativeAd,
17-
}) : super(type: 'ad_feed_item');
15+
const AdFeedItem({required this.id, required this.nativeAd})
16+
: super(type: 'ad_feed_item');
1817

1918
/// A unique identifier for this specific ad instance in the feed.
2019
///
2120
/// This is distinct from the ad unit ID and is used for tracking
2221
/// the ad within the feed.
2322
final String id;
2423

25-
/// The loaded native ad object from the ad network SDK.
24+
/// The loaded native ad object, represented by our generic [app_native_ad.NativeAd] model.
2625
///
2726
/// This object contains the actual ad content and is ready for display.
28-
final NativeAd nativeAd;
27+
final app_native_ad.NativeAd nativeAd;
2928

3029
@override
3130
List<Object?> get props => [id, nativeAd, type];

lib/ads/models/models.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export 'ad_feed_item.dart';
2+
export 'native_ad.dart';

lib/ads/models/native_ad.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:flutter/foundation.dart';
3+
4+
/// {@template native_ad}
5+
/// A generic, provider-agnostic model representing a native advertisement.
6+
///
7+
/// This model decouples the application's core logic from specific ad network
8+
/// SDKs (e.g., Google Mobile Ads). It holds a reference to the original,
9+
/// SDK-specific ad object for rendering purposes.
10+
/// {@endtemplate}
11+
@immutable
12+
class NativeAd extends Equatable {
13+
/// {@macro native_ad}
14+
const NativeAd({required this.id, required this.adObject});
15+
16+
/// A unique identifier for this specific native ad instance.
17+
final String id;
18+
19+
/// The original, SDK-specific ad object.
20+
///
21+
/// This object is passed directly to the ad network's UI widget for rendering.
22+
/// It should be cast back to its specific type (e.g., `google_mobile_ads.NativeAd`)
23+
/// only within the dedicated ad rendering widget for that provider.
24+
final Object adObject;
25+
26+
@override
27+
List<Object?> get props => [id, adObject];
28+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
3+
as app_native_ad;
4+
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
5+
6+
/// {@template admob_native_ad_widget}
7+
/// A widget responsible for rendering a Google AdMob native ad.
8+
///
9+
/// This widget takes our generic [app_native_ad.NativeAd] model, extracts
10+
/// the underlying [admob.NativeAd] object, and uses the AdMob SDK's
11+
/// [admob.AdWidget] to display it. It also handles the lifecycle
12+
/// management of the native ad object.
13+
/// {@endtemplate}
14+
class AdMobNativeAdWidget extends StatefulWidget {
15+
/// {@macro admob_native_ad_widget}
16+
const AdMobNativeAdWidget({required this.nativeAd, super.key});
17+
18+
/// The generic native ad data containing the AdMob-specific ad object.
19+
final app_native_ad.NativeAd nativeAd;
20+
21+
@override
22+
State<AdMobNativeAdWidget> createState() => _AdMobNativeAdWidgetState();
23+
}
24+
25+
class _AdMobNativeAdWidgetState extends State<AdMobNativeAdWidget> {
26+
admob.NativeAd? _admobNativeAd;
27+
bool _isAdLoaded = false;
28+
29+
@override
30+
void initState() {
31+
super.initState();
32+
_initializeAd();
33+
}
34+
35+
@override
36+
void didUpdateWidget(covariant AdMobNativeAdWidget oldWidget) {
37+
super.didUpdateWidget(oldWidget);
38+
// If the underlying ad object changes, dispose the old one and initialize the new one.
39+
if (widget.nativeAd.adObject != oldWidget.nativeAd.adObject) {
40+
_disposeOldAd();
41+
_initializeAd();
42+
}
43+
}
44+
45+
void _initializeAd() {
46+
// Ensure the adObject is of the expected AdMob NativeAd type.
47+
if (widget.nativeAd.adObject is admob.NativeAd) {
48+
_admobNativeAd = widget.nativeAd.adObject as admob.NativeAd;
49+
_isAdLoaded = true;
50+
} else {
51+
_admobNativeAd = null;
52+
_isAdLoaded = false;
53+
// Log an error if the adObject is not the expected type.
54+
debugPrint(
55+
'AdMobNativeAdWidget: Expected admob.NativeAd, but received '
56+
'${widget.nativeAd.adObject.runtimeType}. Ad will not be displayed.',
57+
);
58+
}
59+
}
60+
61+
void _disposeOldAd() {
62+
_admobNativeAd?.dispose();
63+
_admobNativeAd = null;
64+
_isAdLoaded = false;
65+
}
66+
67+
@override
68+
void dispose() {
69+
_disposeOldAd();
70+
super.dispose();
71+
}
72+
73+
@override
74+
Widget build(BuildContext context) {
75+
if (!_isAdLoaded || _admobNativeAd == null) {
76+
// If the ad is not loaded or is of the wrong type, return an empty container.
77+
return const SizedBox.shrink();
78+
}
79+
80+
// Use the AdMob SDK's AdWidget to render the native ad.
81+
return admob.AdWidget(ad: _admobNativeAd!);
82+
}
83+
}

lib/ads/widgets/widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'admob_native_ad_widget.dart';

lib/app/view/app.dart

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,26 @@ class App extends StatelessWidget {
2626
required DataRepository<Source> sourcesRepository,
2727
required DataRepository<UserAppSettings> userAppSettingsRepository,
2828
required DataRepository<UserContentPreferences>
29-
userContentPreferencesRepository,
29+
userContentPreferencesRepository,
3030
required DataRepository<RemoteConfig> remoteConfigRepository,
3131
required DataRepository<User> userRepository,
3232
required KVStorageService kvStorageService,
3333
required AppEnvironment environment,
3434
required AdService adService,
3535
this.demoDataMigrationService,
3636
super.key,
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;
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;
4949

5050
final AuthRepository _authenticationRepository;
5151
final DataRepository<Headline> _headlinesRepository;
@@ -54,7 +54,7 @@ class App extends StatelessWidget {
5454
final DataRepository<Source> _sourcesRepository;
5555
final DataRepository<UserAppSettings> _userAppSettingsRepository;
5656
final DataRepository<UserContentPreferences>
57-
_userContentPreferencesRepository;
57+
_userContentPreferencesRepository;
5858
final DataRepository<RemoteConfig> _appConfigRepository;
5959
final DataRepository<User> _userRepository;
6060
final KVStorageService _kvStorageService;
@@ -83,10 +83,9 @@ class App extends StatelessWidget {
8383
BlocProvider(
8484
create: (context) => AppBloc(
8585
authenticationRepository: context.read<AuthRepository>(),
86-
userAppSettingsRepository:
87-
context.read<DataRepository<UserAppSettings>>(),
88-
appConfigRepository:
89-
context.read<DataRepository<RemoteConfig>>(),
86+
userAppSettingsRepository: context
87+
.read<DataRepository<UserAppSettings>>(),
88+
appConfigRepository: context.read<DataRepository<RemoteConfig>>(),
9089
userRepository: context.read<DataRepository<User>>(),
9190
environment: _environment,
9291
demoDataMigrationService: demoDataMigrationService,

0 commit comments

Comments
 (0)