Skip to content

Commit 0867a37

Browse files
authored
Merge pull request #83 from flutter-news-app-full-source-code/refactor-the-ad-system-to-render-the-native-ad-UI-entirely-within-Flutter
Refactor the ad system to render the native ad UI entirely within flutter
2 parents 6e33a4c + ee51f79 commit 0867a37

29 files changed

+454
-295
lines changed

lib/account/view/saved_headlines_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_blo
66
// HeadlineItemWidget import removed
77
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
88
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
9-
import 'package:flutter_news_app_mobile_client_full_source_code/shared/shared.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/feed_core/feed_core.dart';
1010
import 'package:go_router/go_router.dart';
1111
import 'package:ui_kit/ui_kit.dart';
1212

lib/ads/ad_provider.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
13
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
24
as app_native_ad;
35

@@ -18,11 +20,18 @@ abstract class AdProvider {
1820
/// It handles any necessary setup for the specific ad network.
1921
Future<void> initialize();
2022

21-
/// Loads a native ad.
23+
/// Loads a native ad, optionally tailoring it to a specific style.
2224
///
2325
/// Returns a [app_native_ad.NativeAd] object if an ad is successfully loaded,
2426
/// otherwise returns `null`.
25-
Future<app_native_ad.NativeAd?> loadNativeAd();
27+
///
28+
/// The [imageStyle] is used to select an appropriate native ad template
29+
/// that best matches the visual density of the surrounding content.
30+
/// The [adThemeStyle] provides UI-agnostic theme properties for ad styling.
31+
Future<app_native_ad.NativeAd?> loadNativeAd({
32+
required HeadlineImageStyle imageStyle,
33+
required AdThemeStyle adThemeStyle,
34+
});
2635

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

lib/ads/ad_service.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'package:core/core.dart';
12
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
23
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_feed_item.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
35
import 'package:logging/logging.dart';
46
import 'package:uuid/uuid.dart';
57

@@ -35,15 +37,22 @@ class AdService {
3537

3638
/// Retrieves a loaded native ad wrapped as an [AdFeedItem].
3739
///
38-
/// This method delegates the ad loading to the injected [AdProvider].
40+
/// This method delegates the ad loading to the injected [AdProvider],
41+
/// passing along the desired [imageStyle] to select the correct template.
3942
/// If an ad is successfully loaded, it's wrapped in an [AdFeedItem]
4043
/// with a unique ID.
4144
///
4245
/// Returns an [AdFeedItem] if an ad is available, otherwise `null`.
43-
Future<AdFeedItem?> getAd() async {
46+
Future<AdFeedItem?> getAd({
47+
required HeadlineImageStyle imageStyle,
48+
required AdThemeStyle adThemeStyle,
49+
}) async {
4450
_logger.info('Requesting native ad from AdProvider...');
4551
try {
46-
final nativeAd = await _adProvider.loadNativeAd();
52+
final nativeAd = await _adProvider.loadNativeAd(
53+
imageStyle: imageStyle,
54+
adThemeStyle: adThemeStyle,
55+
);
4756
if (nativeAd != null) {
4857
_logger.info('Native ad successfully loaded and wrapped.');
4958
return AdFeedItem(id: _uuid.v4(), nativeAd: nativeAd);

lib/ads/admob_ad_provider.dart

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'dart:async';
22

3+
import 'package:core/core.dart';
34
import 'package:flutter/foundation.dart';
45
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
57
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
68
as app_native_ad;
79
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
@@ -73,20 +75,32 @@ class AdMobAdProvider implements AdProvider {
7375
}
7476

7577
@override
76-
Future<app_native_ad.NativeAd?> loadNativeAd() async {
78+
@override
79+
Future<app_native_ad.NativeAd?> loadNativeAd({
80+
required HeadlineImageStyle imageStyle,
81+
required AdThemeStyle adThemeStyle,
82+
}) async {
7783
if (_nativeAdUnitId.isEmpty) {
7884
_logger.warning('No native ad unit ID configured for this platform.');
7985
return null;
8086
}
8187

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

90+
final templateType = switch (imageStyle) {
91+
HeadlineImageStyle.largeThumbnail => admob.TemplateType.medium,
92+
_ => admob.TemplateType.small,
93+
};
94+
8495
final completer = Completer<admob.NativeAd?>();
8596

8697
final ad = admob.NativeAd(
8798
adUnitId: _nativeAdUnitId,
88-
factoryId: 'listTile', // This ID must match a factory in your native code
8999
request: const admob.AdRequest(),
100+
nativeTemplateStyle: _createNativeTemplateStyle(
101+
templateType: templateType,
102+
adThemeStyle: adThemeStyle,
103+
),
90104
listener: admob.NativeAdListener(
91105
onAdLoaded: (ad) {
92106
_logger.info('Native Ad loaded successfully.');
@@ -138,10 +152,49 @@ class AdMobAdProvider implements AdProvider {
138152
}
139153

140154
// Map the Google Mobile Ads NativeAd to our generic NativeAd model.
141-
// Only the ID and the raw adObject are stored, as per the simplified model.
142155
return app_native_ad.NativeAd(
143156
id: _uuid.v4(), // Generate a unique ID for our internal model
157+
provider: app_native_ad.AdProviderType.admob, // Set the provider
144158
adObject: googleNativeAd, // Store the original AdMob object
145159
);
146160
}
161+
162+
/// Creates a [NativeTemplateStyle] based on the app's current theme.
163+
///
164+
/// This method maps the application's theme properties (colors, text styles)
165+
/// to the AdMob native ad styling options, ensuring a consistent look and feel.
166+
admob.NativeTemplateStyle _createNativeTemplateStyle({
167+
required admob.TemplateType templateType,
168+
required AdThemeStyle adThemeStyle,
169+
}) {
170+
return admob.NativeTemplateStyle(
171+
templateType: templateType,
172+
mainBackgroundColor: adThemeStyle.mainBackgroundColor,
173+
cornerRadius: adThemeStyle.cornerRadius,
174+
callToActionTextStyle: admob.NativeTemplateTextStyle(
175+
textColor: adThemeStyle.callToActionTextColor,
176+
backgroundColor: adThemeStyle.callToActionBackgroundColor,
177+
style: admob.NativeTemplateFontStyle.normal,
178+
size: adThemeStyle.callToActionTextSize,
179+
),
180+
primaryTextStyle: admob.NativeTemplateTextStyle(
181+
textColor: adThemeStyle.primaryTextColor,
182+
backgroundColor: adThemeStyle.primaryBackgroundColor,
183+
style: admob.NativeTemplateFontStyle.bold,
184+
size: adThemeStyle.primaryTextSize,
185+
),
186+
secondaryTextStyle: admob.NativeTemplateTextStyle(
187+
textColor: adThemeStyle.secondaryTextColor,
188+
backgroundColor: adThemeStyle.secondaryBackgroundColor,
189+
style: admob.NativeTemplateFontStyle.normal,
190+
size: adThemeStyle.secondaryTextSize,
191+
),
192+
tertiaryTextStyle: admob.NativeTemplateTextStyle(
193+
textColor: adThemeStyle.tertiaryTextColor,
194+
backgroundColor: adThemeStyle.tertiaryBackgroundColor,
195+
style: admob.NativeTemplateFontStyle.normal,
196+
size: adThemeStyle.tertiaryTextSize,
197+
),
198+
);
199+
}
147200
}

lib/ads/models/ad_theme_style.dart

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:ui_kit/ui_kit.dart';
4+
5+
/// {@template ad_theme_style}
6+
/// A UI-agnostic data model representing the theme properties required for
7+
/// styling native advertisements.
8+
///
9+
/// This class decouples ad styling logic from Flutter's [ThemeData],
10+
/// allowing theme-related values to be passed to service layers without
11+
/// direct UI context dependencies.
12+
/// {@endtemplate}
13+
class AdThemeStyle extends Equatable {
14+
/// {@macro ad_theme_style}
15+
const AdThemeStyle({
16+
required this.mainBackgroundColor,
17+
required this.cornerRadius,
18+
required this.callToActionTextColor,
19+
required this.callToActionBackgroundColor,
20+
required this.callToActionTextSize,
21+
required this.primaryTextColor,
22+
required this.primaryBackgroundColor,
23+
required this.primaryTextSize,
24+
required this.secondaryTextColor,
25+
required this.secondaryBackgroundColor,
26+
required this.secondaryTextSize,
27+
required this.tertiaryTextColor,
28+
required this.tertiaryBackgroundColor,
29+
required this.tertiaryTextSize,
30+
});
31+
32+
/// Factory constructor to create an [AdThemeStyle] from a Flutter [ThemeData].
33+
///
34+
/// This method acts as an adapter, extracting the relevant styling properties
35+
/// from the UI's theme and mapping them to the UI-agnostic [AdThemeStyle] model.
36+
factory AdThemeStyle.fromTheme(ThemeData theme) {
37+
final colorScheme = theme.colorScheme;
38+
final textTheme = theme.textTheme;
39+
40+
return AdThemeStyle(
41+
mainBackgroundColor: colorScheme.surface,
42+
cornerRadius: AppSpacing.sm,
43+
callToActionTextColor: colorScheme.onPrimary,
44+
callToActionBackgroundColor: colorScheme.primary,
45+
callToActionTextSize: textTheme.labelLarge?.fontSize,
46+
primaryTextColor: colorScheme.onSurface,
47+
primaryBackgroundColor: colorScheme.surface,
48+
primaryTextSize: textTheme.titleMedium?.fontSize,
49+
secondaryTextColor: colorScheme.onSurfaceVariant,
50+
secondaryBackgroundColor: colorScheme.surface,
51+
secondaryTextSize: textTheme.bodyMedium?.fontSize,
52+
tertiaryTextColor: colorScheme.onSurfaceVariant,
53+
tertiaryBackgroundColor: colorScheme.surface,
54+
tertiaryTextSize: textTheme.labelSmall?.fontSize,
55+
);
56+
}
57+
58+
/// The background color for the main ad container.
59+
final Color mainBackgroundColor;
60+
61+
/// The corner radius for the ad container.
62+
final double cornerRadius;
63+
64+
/// The text color for the call-to-action button.
65+
final Color callToActionTextColor;
66+
67+
/// The background color for the call-to-action button.
68+
final Color callToActionBackgroundColor;
69+
70+
/// The font size for the call-to-action text.
71+
final double? callToActionTextSize;
72+
73+
/// The text color for the primary text (e.g., ad headline).
74+
final Color primaryTextColor;
75+
76+
/// The background color for the primary text.
77+
final Color primaryBackgroundColor;
78+
79+
/// The font size for the primary text.
80+
final double? primaryTextSize;
81+
82+
/// The text color for the secondary text (e.g., ad body).
83+
final Color secondaryTextColor;
84+
85+
/// The background color for the secondary text.
86+
final Color secondaryBackgroundColor;
87+
88+
/// The font size for the secondary text.
89+
final double? secondaryTextSize;
90+
91+
/// The text color for the tertiary text (e.g., ad attribution).
92+
final Color tertiaryTextColor;
93+
94+
/// The background color for the tertiary text.
95+
final Color tertiaryBackgroundColor;
96+
97+
/// The font size for the tertiary text.
98+
final double? tertiaryTextSize;
99+
100+
@override
101+
List<Object?> get props => [
102+
mainBackgroundColor,
103+
cornerRadius,
104+
callToActionTextColor,
105+
callToActionBackgroundColor,
106+
callToActionTextSize,
107+
primaryTextColor,
108+
primaryBackgroundColor,
109+
primaryTextSize,
110+
secondaryTextColor,
111+
secondaryBackgroundColor,
112+
secondaryTextSize,
113+
tertiaryTextColor,
114+
tertiaryBackgroundColor,
115+
tertiaryTextSize,
116+
];
117+
}

lib/ads/models/native_ad.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
11
import 'package:equatable/equatable.dart';
22
import 'package:flutter/foundation.dart';
33

4+
/// {@template ad_provider_type}
5+
/// Defines the supported ad network providers.
6+
///
7+
/// This enum is used to identify the source of a [NativeAd] object,
8+
/// allowing the UI to select the correct rendering widget at runtime.
9+
/// {@endtemplate}
10+
enum AdProviderType {
11+
/// Google AdMob provider.
12+
admob,
13+
// Add other providers here in the future, e.g., meta, appLovin
14+
}
15+
416
/// {@template native_ad}
517
/// A generic, provider-agnostic model representing a native advertisement.
618
///
719
/// This model decouples the application's core logic from specific ad network
820
/// SDKs (e.g., Google Mobile Ads). It holds a reference to the original,
9-
/// SDK-specific ad object for rendering purposes.
21+
/// SDK-specific ad object for rendering purposes and a [provider] type
22+
/// to identify its origin.
1023
/// {@endtemplate}
1124
@immutable
1225
class NativeAd extends Equatable {
1326
/// {@macro native_ad}
14-
const NativeAd({required this.id, required this.adObject});
27+
const NativeAd({
28+
required this.id,
29+
required this.provider,
30+
required this.adObject,
31+
});
1532

1633
/// A unique identifier for this specific native ad instance.
1734
final String id;
1835

36+
/// The ad provider that this ad belongs to.
37+
///
38+
/// This is used by the UI to determine which rendering widget to use.
39+
final AdProviderType provider;
40+
1941
/// The original, SDK-specific ad object.
2042
///
2143
/// This object is passed directly to the ad network's UI widget for rendering.
@@ -24,5 +46,5 @@ class NativeAd extends Equatable {
2446
final Object adObject;
2547

2648
@override
27-
List<Object?> get props => [id, adObject];
49+
List<Object?> get props => [id, provider, adObject];
2850
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/models.dart';
3+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/widgets.dart';
4+
import 'package:ui_kit/ui_kit.dart';
5+
6+
/// {@template ad_feed_item_widget}
7+
/// A widget that acts as a dispatcher for rendering native ads from different
8+
/// providers.
9+
///
10+
/// This widget inspects the [AdFeedItem]'s underlying [NativeAd] to determine
11+
/// its [AdProviderType]. It then delegates the rendering to the appropriate
12+
/// provider-specific widget (e.g., [AdmobNativeAdWidget]).
13+
///
14+
/// This approach ensures that the ad rendering logic is decoupled from the
15+
/// main feed UI, making the system extensible to support multiple ad networks.
16+
/// {@endtemplate}
17+
class AdFeedItemWidget extends StatelessWidget {
18+
/// {@macro ad_feed_item_widget}
19+
const AdFeedItemWidget({required this.adFeedItem, super.key});
20+
21+
/// The ad feed item containing the loaded native ad to be displayed.
22+
final AdFeedItem adFeedItem;
23+
24+
@override
25+
Widget build(BuildContext context) {
26+
// The main container for the ad, styled to look like other feed items.
27+
return Card(
28+
margin: const EdgeInsets.symmetric(
29+
horizontal: AppSpacing.paddingMedium,
30+
vertical: AppSpacing.xs,
31+
),
32+
child: ConstrainedBox(
33+
constraints: const BoxConstraints(
34+
minWidth: 320, // Minimum recommended width for ads
35+
minHeight: 90, // Minimum height for a small template ad
36+
),
37+
// The _AdDispatcher is responsible for selecting the correct
38+
// provider-specific widget.
39+
child: _AdDispatcher(nativeAd: adFeedItem.nativeAd),
40+
),
41+
);
42+
}
43+
}
44+
45+
/// A private helper widget that selects the correct ad rendering widget
46+
/// based on the [NativeAd.provider].
47+
class _AdDispatcher extends StatelessWidget {
48+
const _AdDispatcher({required this.nativeAd});
49+
50+
final NativeAd nativeAd;
51+
52+
@override
53+
Widget build(BuildContext context) {
54+
// Use a switch statement on the provider to determine which widget to build.
55+
// This is the core of the platform-agnostic rendering logic.
56+
switch (nativeAd.provider) {
57+
case AdProviderType.admob:
58+
// If the provider is AdMob, render the AdmobNativeAdWidget.
59+
return AdmobNativeAdWidget(nativeAd: nativeAd);
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)