Skip to content

Commit 6e33a4c

Browse files
authored
Merge pull request #82 from flutter-news-app-full-source-code/Implement-Functional,-Style-Synced--native-Ad-Widgets
Implement functional, style synced native ad widgets
2 parents faead38 + b57790a commit 6e33a4c

File tree

8 files changed

+111
-240
lines changed

8 files changed

+111
-240
lines changed

lib/ads/admob_ad_provider.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class AdMobAdProvider implements AdProvider {
2424
final Logger _logger;
2525
final Uuid _uuid = const Uuid();
2626

27+
static const _nativeAdLoadTimeout = 15;
28+
2729
/// The AdMob Native Ad Unit ID for Android.
2830
///
2931
/// This should be replaced with your production Ad Unit ID.
@@ -123,7 +125,7 @@ class AdMobAdProvider implements AdProvider {
123125

124126
// Add a timeout to the future to prevent hanging if callbacks are not called.
125127
final googleNativeAd = await completer.future.timeout(
126-
const Duration(seconds: 15),
128+
const Duration(seconds: _nativeAdLoadTimeout),
127129
onTimeout: () {
128130
_logger.warning('Native ad loading timed out.');
129131
ad.dispose(); // Dispose the ad if it timed out

lib/ads/widgets/admob_native_ad_widget.dart

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
33
as app_native_ad;
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/native_ad_view.dart';
45
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
56

67
/// {@template admob_native_ad_widget}
@@ -11,12 +12,9 @@ import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
1112
/// [admob.AdWidget] to display it. It also handles the lifecycle
1213
/// management of the native ad object.
1314
/// {@endtemplate}
14-
class AdMobNativeAdWidget extends StatefulWidget {
15+
class AdMobNativeAdWidget extends NativeAdView {
1516
/// {@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;
17+
const AdMobNativeAdWidget({required super.nativeAd, super.key});
2018

2119
@override
2220
State<AdMobNativeAdWidget> createState() => _AdMobNativeAdWidgetState();

lib/ads/widgets/native_ad_view.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
5+
/// {@template native_ad_view}
6+
/// An abstract widget that defines the interface for rendering a native ad.
7+
///
8+
/// Concrete implementations of this widget will be responsible for displaying
9+
/// the native ad content from a specific ad network SDK (e.g., AdMob).
10+
/// This abstraction ensures that the higher-level UI components remain
11+
/// provider-agnostic.
12+
/// {@endtemplate}
13+
abstract class NativeAdView extends StatefulWidget {
14+
/// {@macro native_ad_view}
15+
const NativeAdView({required this.nativeAd, super.key});
16+
17+
/// The generic native ad data to display.
18+
///
19+
/// This object contains the original, SDK-specific ad object, which concrete
20+
/// implementations will cast and render.
21+
final app_native_ad.NativeAd nativeAd;
22+
23+
// StatefulWidget requires a createState method, which will be implemented
24+
// by concrete subclasses.
25+
@override
26+
State<NativeAdView> createState();
27+
}

lib/headlines-feed/view/headlines_feed_page.dart

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import 'package:core/core.dart';
55
import 'package:flutter/material.dart';
66
import 'package:flutter_bloc/flutter_bloc.dart';
77
import 'package:flutter_news_app_mobile_client_full_source_code/account/bloc/account_bloc.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_feed_item.dart';
89
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
910
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_feed_bloc.dart';
1011
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
1112
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
12-
import 'package:flutter_news_app_mobile_client_full_source_code/shared/shared.dart';
13+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/ads/ad_feed_item_widget.dart';
14+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/feed_core/feed_core.dart';
1315
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/feed_decorators/call_to_action_decorator_widget.dart';
1416
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/feed_decorators/content_collection_decorator_widget.dart';
1517
import 'package:go_router/go_router.dart';
@@ -287,43 +289,16 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
287289
);
288290
}
289291
return tile;
290-
} else if (item is Ad) {
291-
return Card(
292-
margin: const EdgeInsets.symmetric(
293-
horizontal: AppSpacing.paddingMedium,
294-
vertical: AppSpacing.xs,
295-
),
296-
color: colorScheme.surfaceContainerHighest,
297-
child: Padding(
298-
padding: const EdgeInsets.all(AppSpacing.md),
299-
child: Column(
300-
children: [
301-
if (item.imageUrl.isNotEmpty)
302-
Image.network(
303-
item.imageUrl,
304-
height: 100,
305-
errorBuilder: (ctx, err, st) =>
306-
const Icon(Icons.broken_image, size: 50),
307-
),
308-
const SizedBox(height: AppSpacing.sm),
309-
Text(
310-
'Placeholder Ad: ${item.adType}',
311-
style: textTheme.titleSmall,
312-
),
313-
Text(
314-
'Placement: ${item.placement}',
315-
style: textTheme.bodySmall,
316-
),
317-
if (item.targetUrl.isNotEmpty)
318-
TextButton(
319-
onPressed: () {
320-
// TODO(fulleni): Launch URL
321-
},
322-
child: const Text('Visit Advertiser'),
323-
),
324-
],
325-
),
326-
),
292+
} else if (item is AdFeedItem) {
293+
final imageStyle = context
294+
.watch<AppBloc>()
295+
.state
296+
.settings
297+
.feedPreferences
298+
.headlineImageStyle;
299+
return AdFeedItemWidget(
300+
adFeedItem: item,
301+
headlineImageStyle: imageStyle,
327302
);
328303
} else if (item is CallToActionItem) {
329304
return CallToActionDecoratorWidget(
Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,63 @@
1+
import 'package:core/core.dart';
12
import 'package:flutter/material.dart';
23
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_feed_item.dart';
34
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/admob_native_ad_widget.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/native_ad_view.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/ads/ads.dart';
47
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
5-
import 'package:ui_kit/ui_kit.dart';
8+
import 'package:logging/logging.dart';
69

710
/// {@template ad_feed_item_widget}
8-
/// A widget responsible for rendering a native ad within the feed.
11+
/// A widget responsible for rendering a native ad within the feed,
12+
/// adapting its appearance based on the [HeadlineImageStyle] setting.
913
///
1014
/// This widget acts as a dispatcher, taking an [AdFeedItem] and delegating
1115
/// the actual rendering to the appropriate provider-specific ad widget
12-
/// (e.g., [AdMobNativeAdWidget]).
16+
/// (e.g., [AdMobNativeAdWidget]), which is then wrapped by a style-matching
17+
/// ad card.
1318
/// {@endtemplate}
1419
class AdFeedItemWidget extends StatelessWidget {
1520
/// {@macro ad_feed_item_widget}
16-
const AdFeedItemWidget({required this.adFeedItem, super.key});
21+
const AdFeedItemWidget({
22+
required this.adFeedItem,
23+
required this.headlineImageStyle,
24+
super.key,
25+
});
1726

1827
/// The ad feed item containing the loaded native ad to be displayed.
1928
final AdFeedItem adFeedItem;
2029

30+
/// The preferred image style for headlines, used to match the ad's appearance.
31+
final HeadlineImageStyle headlineImageStyle;
32+
2133
@override
2234
Widget build(BuildContext context) {
23-
// Determine the type of the underlying ad object to dispatch to the
24-
// correct rendering widget.
25-
// For now, we only support AdMob, but this can be extended.
35+
// Determine the type of the underlying ad object to instantiate the
36+
// correct provider-specific NativeAdView.
37+
final NativeAdView? nativeAdView;
2638
if (adFeedItem.nativeAd.adObject is admob.NativeAd) {
27-
return Card(
28-
margin: const EdgeInsets.symmetric(
29-
vertical: AppSpacing.sm,
30-
horizontal: AppSpacing.lg,
31-
),
32-
child: SizedBox(
33-
height: 120, // Fixed height for the ad card
34-
child: AdMobNativeAdWidget(nativeAd: adFeedItem.nativeAd),
35-
),
36-
);
39+
nativeAdView = AdMobNativeAdWidget(nativeAd: adFeedItem.nativeAd);
3740
} else {
38-
// Fallback for unsupported ad types or if adObject is null/unexpected.
39-
// In a production app, you might log this or show a generic error ad.
40-
debugPrint(
41-
'AdFeedItemWidget: Unsupported native ad type: '
42-
'${adFeedItem.nativeAd.adObject.runtimeType}.',
41+
// Log an error for unsupported ad types.
42+
Logger('AdFeedItemWidget').warning(
43+
'Unsupported native ad type: ${adFeedItem.nativeAd.adObject.runtimeType}. '
44+
'Ad will not be displayed.',
4345
);
46+
nativeAdView = null;
47+
}
48+
49+
if (nativeAdView == null) {
4450
return const SizedBox.shrink();
4551
}
52+
53+
// Select the appropriate ad card widget based on the headline image style.
54+
switch (headlineImageStyle) {
55+
case HeadlineImageStyle.hidden:
56+
return NativeAdCardTextOnly(adView: nativeAdView);
57+
case HeadlineImageStyle.smallThumbnail:
58+
return NativeAdCardImageStart(adView: nativeAdView);
59+
case HeadlineImageStyle.largeThumbnail:
60+
return NativeAdCardImageTop(adView: nativeAdView);
61+
}
4662
}
4763
}
Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,29 @@
11
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;
2+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/native_ad_view.dart';
43
import 'package:ui_kit/ui_kit.dart';
54

65
/// {@template native_ad_card_image_start}
7-
/// A generic widget to display a native ad with a small image at the start.
6+
/// A widget to display a native ad with a small image at the start,
7+
/// visually mimicking [HeadlineTileImageStart].
88
///
9-
/// This widget is designed to visually match [HeadlineTileImageStart]
10-
/// and uses a generic [app_native_ad.NativeAd] model.
9+
/// This widget accepts a [NativeAdView] to render the actual ad content,
10+
/// ensuring it remains provider-agnostic.
1111
/// {@endtemplate}
1212
class NativeAdCardImageStart extends StatelessWidget {
1313
/// {@macro native_ad_card_image_start}
14-
const NativeAdCardImageStart({required this.nativeAd, super.key});
14+
const NativeAdCardImageStart({required this.adView, super.key});
1515

16-
/// The generic native ad data to display.
17-
final app_native_ad.NativeAd nativeAd;
16+
/// The widget responsible for rendering the native ad content.
17+
final NativeAdView adView;
1818

1919
@override
2020
Widget build(BuildContext context) {
21-
final theme = Theme.of(context);
22-
final textTheme = theme.textTheme;
23-
final colorScheme = theme.colorScheme;
24-
25-
// Placeholder content for the generic ad.
26-
// The actual rendering of the SDK-specific ad will happen in a child widget.
2721
return Card(
2822
margin: const EdgeInsets.symmetric(
2923
horizontal: AppSpacing.paddingMedium,
3024
vertical: AppSpacing.xs,
3125
),
32-
child: Padding(
33-
padding: const EdgeInsets.all(AppSpacing.md),
34-
child: Row(
35-
crossAxisAlignment: CrossAxisAlignment.start,
36-
children: [
37-
SizedBox(
38-
width: 72, // Standard small image size
39-
height: 72,
40-
child: ClipRRect(
41-
borderRadius: BorderRadius.circular(AppSpacing.xs),
42-
child: ColoredBox(
43-
color: colorScheme.surfaceContainerHighest,
44-
child: Icon(
45-
Icons.campaign_outlined,
46-
color: colorScheme.onSurfaceVariant,
47-
size: AppSpacing.xl,
48-
),
49-
),
50-
),
51-
),
52-
const SizedBox(width: AppSpacing.md), // Always add spacing
53-
Expanded(
54-
child: Column(
55-
crossAxisAlignment: CrossAxisAlignment.start,
56-
children: [
57-
Text(
58-
'Ad: ${nativeAd.id}', // Displaying ID for now
59-
style: textTheme.titleMedium?.copyWith(
60-
fontWeight: FontWeight.w500,
61-
),
62-
maxLines: 2,
63-
overflow: TextOverflow.ellipsis,
64-
),
65-
const SizedBox(height: AppSpacing.sm),
66-
Text(
67-
'This is a generic ad placeholder.',
68-
style: textTheme.bodySmall?.copyWith(
69-
color: colorScheme.primary.withOpacity(0.7),
70-
),
71-
maxLines: 2,
72-
overflow: TextOverflow.ellipsis,
73-
),
74-
],
75-
),
76-
),
77-
],
78-
),
79-
),
26+
child: adView, // Directly render the provided NativeAdView
8027
);
8128
}
8229
}

0 commit comments

Comments
 (0)