Skip to content

Commit 21addca

Browse files
authored
Merge pull request #130 from flutter-news-app-full-source-code/AA
refactor/ui-enhancements
2 parents 6a0fd2b + c19722d commit 21addca

File tree

9 files changed

+243
-208
lines changed

9 files changed

+243
-208
lines changed

lib/ads/inline_ad_cache_service.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,10 @@ class InlineAdCacheService {
127127
'Clearing all cached inline ads and disposing their resources.',
128128
);
129129
for (final ad in _cache.values.whereType<InlineAd>()) {
130-
131130
// Delegate disposal to AdService
132131
_adService.disposeAd(ad);
133132
}
134-
133+
135134
// Ensure cache is empty after disposal attempts.
136135
_cache.clear();
137136
_logger.info('All cached inline ads cleared.');

lib/app/view/app.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:auth_repository/auth_repository.dart';
22
import 'package:core/core.dart' hide AppStatus;
33
import 'package:data_repository/data_repository.dart';
4-
import 'package:flex_color_scheme/flex_color_scheme.dart';
54
import 'package:flutter/material.dart';
65
import 'package:flutter_bloc/flutter_bloc.dart';
76
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart';

lib/headline-details/view/headline_details_page.dart

Lines changed: 72 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,17 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
311311
),
312312
),
313313
),
314+
SliverPadding(
315+
padding: horizontalPadding.copyWith(top: AppSpacing.md),
316+
sliver: SliverToBoxAdapter(
317+
child: Text(
318+
DateFormat('yyyy/MM/dd').format(headline.createdAt),
319+
style: textTheme.bodyMedium?.copyWith(
320+
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
321+
),
322+
),
323+
),
324+
),
314325
SliverPadding(
315326
padding: EdgeInsets.only(
316327
top: AppSpacing.md,
@@ -350,10 +361,25 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
350361
SliverPadding(
351362
padding: horizontalPadding.copyWith(top: AppSpacing.lg),
352363
sliver: SliverToBoxAdapter(
353-
child: Wrap(
354-
spacing: AppSpacing.md,
355-
runSpacing: AppSpacing.sm,
356-
children: _buildMetadataChips(context, headline, onEntityChipTap),
364+
child: SizedBox(
365+
height: 36,
366+
child: BlocBuilder<HeadlineDetailsBloc, HeadlineDetailsState>(
367+
builder: (context, state) {
368+
final chips = _buildMetadataChips(
369+
context,
370+
headline,
371+
onEntityChipTap,
372+
);
373+
return ListView.separated(
374+
scrollDirection: Axis.horizontal,
375+
itemCount: chips.length,
376+
separatorBuilder: (context, index) =>
377+
const SizedBox(width: AppSpacing.sm),
378+
itemBuilder: (context, index) => chips[index],
379+
clipBehavior: Clip.none,
380+
);
381+
},
382+
),
357383
),
358384
),
359385
),
@@ -540,102 +566,54 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
540566
final colorScheme = theme.colorScheme;
541567
final chipLabelStyle = textTheme.labelMedium?.copyWith(
542568
color: colorScheme.onSecondaryContainer,
569+
fontWeight: FontWeight.w600,
543570
);
544-
final chipBackgroundColor = colorScheme.secondaryContainer;
571+
final chipBackgroundColor = colorScheme.secondaryContainer.withOpacity(0.6);
545572
final chipAvatarColor = colorScheme.onSecondaryContainer;
546-
const chipAvatarSize = AppSpacing.md;
547-
const chipPadding = EdgeInsets.symmetric(
548-
horizontal: AppSpacing.sm,
549-
vertical: AppSpacing.xs,
550-
);
551-
final chipShape = RoundedRectangleBorder(
552-
borderRadius: BorderRadius.circular(AppSpacing.sm),
553-
side: BorderSide(color: colorScheme.outlineVariant.withOpacity(0.3)),
554-
);
555-
556-
final chips = <Widget>[];
557-
558-
final formattedDate = DateFormat('MMM d, yyyy').format(headline.createdAt);
559-
chips
560-
..add(
561-
Chip(
562-
avatar: Icon(
563-
Icons.calendar_today_outlined,
564-
size: chipAvatarSize,
565-
color: chipAvatarColor,
566-
),
567-
label: Text(formattedDate),
568-
labelStyle: chipLabelStyle,
569-
backgroundColor: chipBackgroundColor,
570-
padding: chipPadding,
571-
shape: chipShape,
572-
visualDensity: VisualDensity.compact,
573-
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
574-
),
575-
)
576-
..add(
577-
InkWell(
578-
onTap: () => onEntityChipTap(ContentType.source, headline.source.id),
579-
borderRadius: BorderRadius.circular(AppSpacing.sm),
580-
child: Chip(
581-
avatar: Icon(
582-
Icons.source_outlined,
583-
size: chipAvatarSize,
584-
color: chipAvatarColor,
585-
),
586-
label: Text(headline.source.name),
587-
labelStyle: chipLabelStyle,
588-
backgroundColor: chipBackgroundColor,
589-
padding: chipPadding,
590-
shape: chipShape,
591-
visualDensity: VisualDensity.compact,
592-
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
593-
),
573+
const chipAvatarSize = 18.0;
574+
575+
Widget buildChip({
576+
required IconData icon,
577+
required String label,
578+
required VoidCallback onPressed,
579+
}) {
580+
return ActionChip(
581+
avatar: Icon(icon, size: chipAvatarSize, color: chipAvatarColor),
582+
label: Text(label),
583+
labelStyle: chipLabelStyle,
584+
backgroundColor: chipBackgroundColor,
585+
onPressed: onPressed,
586+
visualDensity: VisualDensity.compact,
587+
padding: const EdgeInsets.symmetric(
588+
horizontal: AppSpacing.sm,
589+
vertical: AppSpacing.xs,
594590
),
595-
)
596-
..add(
597-
InkWell(
598-
onTap: () => onEntityChipTap(ContentType.topic, headline.topic.id),
599-
borderRadius: BorderRadius.circular(AppSpacing.sm),
600-
child: Chip(
601-
avatar: Icon(
602-
Icons.category_outlined,
603-
size: chipAvatarSize,
604-
color: chipAvatarColor,
605-
),
606-
label: Text(headline.topic.name),
607-
labelStyle: chipLabelStyle,
608-
backgroundColor: chipBackgroundColor,
609-
padding: chipPadding,
610-
shape: chipShape,
611-
visualDensity: VisualDensity.compact,
612-
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
613-
),
614-
),
615-
)
616-
..add(
617-
InkWell(
618-
onTap: () =>
619-
onEntityChipTap(ContentType.country, headline.eventCountry.id),
620-
borderRadius: BorderRadius.circular(AppSpacing.sm),
621-
child: Chip(
622-
avatar: Icon(
623-
Icons.location_city_outlined,
624-
size: chipAvatarSize,
625-
color: chipAvatarColor,
626-
),
627-
label: Text(headline.eventCountry.name),
628-
labelStyle: chipLabelStyle,
629-
backgroundColor: chipBackgroundColor,
630-
padding: chipPadding,
631-
shape: chipShape,
632-
visualDensity: VisualDensity.compact,
633-
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
634-
),
591+
shape: RoundedRectangleBorder(
592+
borderRadius: BorderRadius.circular(AppSpacing.lg),
593+
side: BorderSide.none,
635594
),
636595
);
596+
}
637597

638-
return chips;
598+
return [
599+
buildChip(
600+
icon: Icons.source_outlined,
601+
label: headline.source.name,
602+
onPressed: () =>
603+
onEntityChipTap(ContentType.source, headline.source.id),
604+
),
605+
buildChip(
606+
icon: Icons.category_outlined,
607+
label: headline.topic.name,
608+
onPressed: () => onEntityChipTap(ContentType.topic, headline.topic.id),
609+
),
610+
buildChip(
611+
icon: Icons.location_city_outlined,
612+
label: headline.eventCountry.name,
613+
onPressed: () =>
614+
onEntityChipTap(ContentType.country, headline.eventCountry.id),
615+
),
616+
];
639617
}
640618

641619
Widget _buildSimilarHeadlinesSection(

lib/headlines-feed/view/headlines_feed_page.dart

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
//
2-
// ignore_for_file: lines_longer_than_80_chars
3-
41
import 'package:core/core.dart';
52
import 'package:flutter/material.dart';
63
import 'package:flutter_bloc/flutter_bloc.dart';
7-
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
84
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_placeholder.dart';
95
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
106
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/feed_ad_loader_widget.dart';
@@ -237,23 +233,6 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
237233
);
238234
}
239235

240-
Future<void> onHeadlineTap(Headline headline) async {
241-
// Await for the ad to be shown and dismissed.
242-
await context
243-
.read<InterstitialAdManager>()
244-
.onPotentialAdTrigger();
245-
246-
// Check if the widget is still in the tree before navigating.
247-
if (!context.mounted) return;
248-
249-
// Proceed with navigation after the ad is closed.
250-
await context.pushNamed(
251-
Routes.articleDetailsName,
252-
pathParameters: {'id': headline.id},
253-
extra: headline,
254-
);
255-
}
256-
257236
return RefreshIndicator(
258237
onRefresh: () async {
259238
context.read<HeadlinesFeedBloc>().add(
@@ -272,15 +251,16 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
272251
? state.feedItems.length + 1
273252
: state.feedItems.length,
274253
separatorBuilder: (context, index) {
275-
if (index < state.feedItems.length - 1) {
276-
final currentItem = state.feedItems[index];
277-
final nextItem = state.feedItems[index + 1];
278-
// Adjust spacing around any decorator or ad
279-
if (currentItem is! Headline || nextItem is! Headline) {
280-
return const SizedBox(height: AppSpacing.md);
281-
}
254+
if (index >= state.feedItems.length - 1) {
255+
return const SizedBox.shrink();
256+
}
257+
final currentItem = state.feedItems[index];
258+
final nextItem = state.feedItems[index + 1];
259+
260+
if (currentItem is! Headline || nextItem is! Headline) {
261+
return const SizedBox(height: AppSpacing.md);
282262
}
283-
return const SizedBox(height: AppSpacing.lg);
263+
return const SizedBox(height: AppSpacing.sm);
284264
},
285265
itemBuilder: (context, index) {
286266
if (index >= state.feedItems.length) {
@@ -305,17 +285,29 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
305285
case HeadlineImageStyle.hidden:
306286
tile = HeadlineTileTextOnly(
307287
headline: item,
308-
onHeadlineTap: () => onHeadlineTap(item),
288+
onHeadlineTap: () =>
289+
HeadlineTapHandler.handleHeadlineTap(
290+
context,
291+
item,
292+
),
309293
);
310294
case HeadlineImageStyle.smallThumbnail:
311295
tile = HeadlineTileImageStart(
312296
headline: item,
313-
onHeadlineTap: () => onHeadlineTap(item),
297+
onHeadlineTap: () =>
298+
HeadlineTapHandler.handleHeadlineTap(
299+
context,
300+
item,
301+
),
314302
);
315303
case HeadlineImageStyle.largeThumbnail:
316304
tile = HeadlineTileImageTop(
317305
headline: item,
318-
onHeadlineTap: () => onHeadlineTap(item),
306+
onHeadlineTap: () =>
307+
HeadlineTapHandler.handleHeadlineTap(
308+
context,
309+
item,
310+
),
319311
);
320312
}
321313
return tile;

lib/shared/widgets/feed_core/feed_core.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export 'headline_metadata_row.dart';
1+
export 'headline_source_row.dart';
22
export 'headline_tap_handler.dart';
33
export 'headline_tile_image_start.dart';
44
export 'headline_tile_image_top.dart';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
7+
import 'package:go_router/go_router.dart';
8+
import 'package:timeago/timeago.dart' as timeago;
9+
import 'package:ui_kit/ui_kit.dart';
10+
11+
/// {@template headline_source_row}
12+
/// A widget to display the source and publish date of a headline.
13+
/// {@endtemplate}
14+
class HeadlineSourceRow extends StatelessWidget {
15+
/// {@macro headline_source_row}
16+
const HeadlineSourceRow({required this.headline, super.key});
17+
18+
/// The headline data to display.
19+
final Headline headline;
20+
21+
Future<void> _handleEntityTap(BuildContext context) async {
22+
await context.read<InterstitialAdManager>().onPotentialAdTrigger();
23+
if (!context.mounted) return;
24+
await context.pushNamed(
25+
Routes.entityDetailsName,
26+
pathParameters: {
27+
'type': ContentType.source.name,
28+
'id': headline.source.id,
29+
},
30+
);
31+
}
32+
33+
@override
34+
Widget build(BuildContext context) {
35+
final theme = Theme.of(context);
36+
final textTheme = theme.textTheme;
37+
final colorScheme = theme.colorScheme;
38+
final currentLocale = context.watch<AppBloc>().state.locale;
39+
40+
final formattedDate = timeago.format(
41+
headline.createdAt,
42+
locale: currentLocale.languageCode,
43+
);
44+
45+
final sourceTextStyle = textTheme.bodySmall?.copyWith(
46+
color: colorScheme.onSurfaceVariant,
47+
fontWeight: FontWeight.w500,
48+
);
49+
50+
final dateTextStyle = textTheme.bodySmall?.copyWith(
51+
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
52+
);
53+
54+
return Row(
55+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
56+
children: [
57+
Expanded(
58+
child: InkWell(
59+
onTap: () => _handleEntityTap(context),
60+
child: Row(
61+
mainAxisSize: MainAxisSize.min,
62+
children: [
63+
Icon(
64+
Icons.source_outlined,
65+
size: AppSpacing.md,
66+
color: colorScheme.onSurfaceVariant,
67+
),
68+
const SizedBox(width: AppSpacing.xs),
69+
Flexible(
70+
child: Text(
71+
headline.source.name,
72+
style: sourceTextStyle,
73+
overflow: TextOverflow.ellipsis,
74+
),
75+
),
76+
],
77+
),
78+
),
79+
),
80+
if (formattedDate.isNotEmpty) Text(formattedDate, style: dateTextStyle),
81+
],
82+
);
83+
}
84+
}

0 commit comments

Comments
 (0)