Flutter for OpenHarmony:从零搭建今日资讯App(五)新闻卡片组件
新闻卡片组件实现要点解析 摘要: 本文介绍了新闻卡片组件的核心实现细节。该组件采用StatelessWidget构建,包含图片加载(支持缓存和占位图)、标题摘要(限制2行并显示省略号)、来源和时间信息展示等功能。关键实现包括:使用CachedNetworkImage优化图片加载性能,通过InkWell提供水波纹点击效果,固定16:9的图片宽高比保证布局统一,以及完善的错误处理机制。组件设计遵循Ma

新闻卡片是首页的核心组件,负责展示新闻的图片、标题、摘要、来源和时间。虽然看起来简单,但要做好需要考虑很多细节——图片加载、文本处理、时间格式化、点击交互等。
新闻卡片展示内容
在动手写代码之前,先分析一下新闻卡片需要展示哪些信息:
新闻图片 - 吸引眼球的配图,支持网络加载、缓存、占位图、错误处理
新闻标题 - 简洁有力的标题,最多显示两行,超出部分用省略号
新闻摘要 - 简短的内容概述,同样最多两行
新闻来源 - 告诉用户这条新闻来自哪里,增加可信度
发布时间 - 显示相对时间(比如"2小时前"),更直观
点击交互 - 整个卡片可点击,有点击反馈
导入依赖
创建lib/widgets/news_card.dart:
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import '../models/news_article.dart';
import '../screens/news_detail/news_detail_screen.dart';
代码解析:
cached_network_image- 图片缓存库,自动缓存网络图片,提升加载速度intl- 日期格式化库,用于时间格式化news_article- 新闻数据模型news_detail_screen- 详情页
为什么放在widgets目录:因为这是个可复用的组件,不仅首页要用,搜索页、收藏页也会用到。
定义组件结构
class NewsCard extends StatelessWidget {
final NewsArticle article;
const NewsCard({super.key, required this.article});
代码解析:
- 使用
StatelessWidget,只负责展示数据,不需要管理状态 article参数必需,包含新闻的所有信息required关键字明确表达这个参数是必需的
为什么用StatelessWidget:卡片只是展示数据,数据由外部传入,组件本身不需要维护状态。用StatelessWidget性能更好,因为Flutter可以更好地优化它。
构建卡片整体结构
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => NewsDetailScreen(article: article),
),
);
},
borderRadius: BorderRadius.circular(12),
代码解析:
Card- Flutter提供的卡片组件,自带阴影、圆角、背景色等Material Design效果InkWell- 提供水波纹点击效果,比GestureDetector体验更好borderRadius- 圆角要和Card一致(12),否则水波纹会超出边界- 点击跳转到详情页,传递article数据
InkWell vs GestureDetector:两者都能处理点击事件,但InkWell有水波纹效果,更符合Material Design规范。点击时会有一圈圈扩散的动画,用户体验更好。
构建卡片内容
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (article.imageUrl != null) _buildImage(),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
const SizedBox(height: 8),
_buildSummary(),
const SizedBox(height: 12),
_buildMetadata(context),
],
),
),
],
),
),
);
}
代码解析:
crossAxisAlignment.start- 所有子Widget左对齐,如果不设置,默认是居中if (article.imageUrl != null)- 条件渲染,有图片才显示,这是Dart 2.3引入的collection if语法- 图片占满宽度,文字部分四周padding 12
SizedBox控制间距,8和12是经过调整的最佳值
实现图片加载
Widget _buildImage() {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: article.imageUrl != null
? CachedNetworkImage(
imageUrl: article.imageUrl!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey[300],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => _buildPlaceholder(),
)
: _buildPlaceholder(),
),
);
}
代码解析:
ClipRRect- 裁剪子Widget,实现圆角,Image组件本身不支持borderRadiusBorderRadius.vertical(top:)- 只设置顶部圆角,因为图片在卡片顶部AspectRatio(16/9)- 固定宽高比,保证所有卡片图片高度一致,列表看起来更整齐CachedNetworkImage- 自动缓存图片,第二次加载直接从缓存读取,速度快很多BoxFit.cover- 填满区域,超出部分裁剪,不变形placeholder- 加载中显示灰色背景和转圈动画errorWidget- 加载失败显示占位图
为什么固定宽高比:如果不固定,图片按原始尺寸显示,每个卡片高度不一样,列表很乱。固定16:9后,所有图片高度一致,而且16:9是视频和图片的标准比例。
创建占位图
Widget _buildPlaceholder() {
return Container(
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.article_outlined,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 8),
Text(
'新闻图片',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
),
);
}
代码解析:
- 浅灰色背景,中间显示文章图标和文字
- 用灰色系表示这是占位内容,不是真实图片
- 图标大小48,在16:9区域显示刚好
为什么要有占位图:如果图片加载失败就显示空白,用户会以为卡片坏了。有个占位图,用户知道这里本来应该有图片,只是加载失败了。这是个很重要的用户体验细节。
实现标题和摘要
Widget _buildTitle() {
return Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
Widget _buildSummary() {
return Text(
article.summary,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
height: 1.4,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
代码解析:
- 标题字号16,加粗显示,让标题更醒目
- 摘要字号14,灰色文字,形成层次感
height- 行高,标题1.3,摘要1.4,多行文字不会挤在一起maxLines: 2- 最多显示两行,一行太少,三行太多overflow: TextOverflow.ellipsis- 超出部分显示省略号
为什么是两行:一行太少,很多标题显示不完;三行太多,占用空间大。两行是个平衡点,既能显示足够信息,又不会太占空间。
实现元数据显示
Widget _buildMetadata(BuildContext context) {
final publishedDate = DateTime.tryParse(article.publishedAt);
final dateStr = publishedDate != null
? _formatDate(publishedDate)
: '未知时间';
return Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
article.source,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(width: 8),
Icon(Icons.access_time, size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
dateStr,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
);
}
代码解析:
DateTime.tryParse- 尝试解析日期,失败返回null而不抛异常,比DateTime.parse安全- 来源标签用Container包裹,加背景色和圆角,看起来像个标签
- 使用主题颜色,自动适配深色模式
- 时钟图标表示时间,通用设计语言,用户一看就懂
- 字号12,元数据是次要信息,字体要小一些
实现时间格式化
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inMinutes < 60) {
return '${difference.inMinutes}分钟前';
} else if (difference.inHours < 24) {
return '${difference.inHours}小时前';
} else if (difference.inDays < 7) {
return '${difference.inDays}天前';
} else {
return DateFormat('MM-dd').format(date);
}
}
}
代码解析:
- 计算当前时间和发布时间的差值
- 1小时内显示分钟,24小时内显示小时,7天内显示天数
- 超过7天显示具体日期(月-日)
- 相对时间更直观,符合用户习惯
为什么这样划分:这是参考了微信、微博等应用的做法。1小时内的新闻很新鲜,要精确到分钟;超过一周的新闻不算新了,显示具体日期就行。
图片加载优化
使用cached_network_image:
- 自动缓存图片到本地
- 第二次加载直接从缓存读取
- 大大提升加载速度
设置固定宽高比:
- 用AspectRatio固定16:9比例
- 避免图片加载完成后卡片高度突变
- 保证列表整齐,滚动流畅
提供占位图:
- 加载中显示占位符
- 加载失败显示占位图
- 用户体验更好
文本处理技巧
限制行数 - 标题和摘要都限制2行,保证卡片高度一致
处理溢出 - 使用TextOverflow.ellipsis,超出部分显示省略号
设置行高 - 标题行高1.3,摘要行高1.4,提升可读性
字体大小层次 - 标题16号,摘要14号,元数据12号,信息主次分明
颜色和主题
使用主题颜色 - 来源标签使用primaryContainer和onPrimaryContainer,自动适配深色模式
灰色层次 - 摘要、时间、占位图使用不同深度的灰色,形成视觉层次
对比度考虑 - 文字颜色要和背景有足够的对比度,否则看不清
交互反馈
InkWell水波纹 - 点击时有扩散动画,符合Material Design规范
borderRadius一致 - 水波纹圆角和Card一致,不会超出边界
导航动画 - 跳转详情页有滑动动画,MaterialPageRoute自带
组件设计原则
职责单一 - 新闻卡片只负责展示数据和处理点击,不负责数据加载
可复用 - 只要传入NewsArticle数据,就能正常显示,不依赖特定上下文
易于维护 - 拆分成多个方法,每个方法职责明确
注重细节 - 图片的圆角、文字的行高、颜色的深浅、间距的大小,这些细节决定了组件的质量
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里你可以找到更多Flutter开发资源,与其他开发者交流经验,共同进步。
更多推荐
所有评论(0)