HarmonyOS新闻卡片组件开发实战:自定义组件与List渲染深度解析
文章目录
- 为什么“死磕”这个新闻项目?
- 🚀 项目效果“卖家秀”
- 跟我一步步“肝”代码
- 第一步:设计新闻“身份证”(数据模型)
- 第二步:造“轮子”!@Component 自定义卡片
- 第三步:卡片“颜值担当”——图片区(Stack 布局)
- 第四步:卡片“灵魂”——内容区(Column + Row)
- 第四步(续):卡片的“脚”——底部信息栏(交互核心)
- 第五步:“组装” App!——主页面架构
- 第六步:实现“横向滚动”的分类筛选条
- 第七步:“魔法”发生地!—— 响应式新闻列表
- 😱 “萌新”踩坑(含泪)实录
- 巨坑 1:我改了“爹”的数据!(@Prop vs @State)
- 小坑 2:图片加载“翻车”
- 小坑 3:布局“偏心眼”
- “精装修”建议(下一步玩啥)
- 总结:你又“行”了!
摘要: 本文详细讲解HarmonyOS新闻卡片组件的完整开发流程,涵盖自定义组件设计、List列表渲染、分类筛选功能实现。通过具体代码示例,分享新闻数据模型构建、复杂布局技巧、父子组件通信机制以及交互体验优化。适合HarmonyOS初学者学习组件化开发和列表展示功能,帮助开发者快速掌握构建现代资讯类应用的核心技术。
标签: HarmonyOS 新闻卡片 自定义组件 List渲染 分类筛选 组件通信 移动开发
大家好!鸿蒙学习“肝帝”又上线了!今天继续我的 HarmonyOS 学习之旅,这次的挑战是——搞一个功能“全家桶”的新闻资讯 App。
这个项目包含了自定义组件、List 渲染、分类筛选等超多实用技巧,绝对是“进阶”必备!特别适合想从“游击队”变成“正规军”,深入学习组件化开发的小伙伴!
为什么“死磕”这个新闻项目?
上次的“国风” Demo 只是开胃菜,这次我们要“来真的”了!
为啥选这个?因为“麻雀虽小,五脏俱全”啊!做一个资讯 App,我(和你们)可以一举拿下:
- ✅ 怎么“造零件”:掌握
@Component装饰器,造一个“高复用”卡片! - ✅ 怎么“刷列表”:精通
List+ForEach,还要学会用filter玩筛选! - ✅ 怎么“叠罗汉”:
Stack、Column、Row嵌套使用,搞定复杂布局! - ✅ 怎么“点个赞”:实现点赞、分类这种真实的用户交互!
🚀 项目效果“卖家秀”
先上个“卖家秀”!想象一下:打开 App,一个专业的“今日头条”风界面映入眼帘。顶部是标题栏和(能横向滚动的)分类筛选条,下面是颜值超高的瀑布流新闻卡片。

每张卡片都有图有真相,标题、摘要、发布时间、阅读量、点赞功能一应俱全。点一下“科技”,列表“唰”一下就只剩科技新闻,丝滑!
跟我一步步“肝”代码
第一步:设计新闻“身份证”(数据模型)

老规矩,“兵马未动,粮草先行”。写 App 之前,先得搞清楚咱们的数据长啥样。
class NewsItem {id: number = 0;title: string = '';summary: string = '';imageUrl: string = '';category: string = '';publishTime: string = '';readCount: number = 0;isLiked: boolean = false;constructor(id: number,title: string,summary: string,imageUrl: string,category: string,publishTime: string,readCount: number,isLiked: boolean) {this.id = id;this.title = title;this.summary = summary;this.imageUrl = imageUrl;this.category = category;this.publishTime = publishTime;this.readCount = readCount;this.isLiked = isLiked;}
}
笔记:
这个 NewsItem 类就是咱们的新闻“身份证”模板。比上次的“国风”商品复杂了点,加了图片 URL、阅读量、是不是点过赞… 毕竟是新闻嘛,要素得齐全!
第二步:造“轮子”!@Component 自定义卡片
OK,核心中的核心来了!我们要“造轮子”——一个可复用的“新闻卡片”组件 buildNewsCard。

这次我们不用上次的 @Builder 了,而是用更“重量级”的 @Component。
为啥?因为 @Builder 只是个“UI 蓝图”(函数),而 @Component 是一个拥有自己生命周期和状态的“独立公民”(结构体)!这对于需要内部交互(比如点赞)的组件来说,至关重要。
@Component
struct buildNewsCard {@Prop news: NewsItem // @Prop: “爹给的”数据(父组件传来)@State localNews: NewsItem = new NewsItem(0, '', '', '', '', '', 0, false) // @State: “自己的”数据(内部状态)aboutToAppear() {// 这是组件“出生”时会调用的方法if (this.news) {// 把“爹给的”数据,复制一份到“自己的”地盘上this.localNews = new NewsItem(this.news.id,this.news.title,this.news.summary,this.news.imageUrl,this.news.category,this.news.publishTime,this.news.readCount,this.news.isLiked);}}// ... build() 方法马上就来 ...
}
笔记:
注意看 @Prop 和 @State 的用法!@Prop 是“爹给的”(父组件传来的),@State 是“自己私有的”(内部状态)。
我们在 aboutToAppear 这个“出生”生命周期方法里,把“爹给的”news 数据“复制”一份到“自己的”localNews 里。这样,我们就可以在组件内部随便“折腾”(比如点赞)这个 localNews,还不会“坑爹”(污染父组件的数据)!
第三步:卡片“颜值担当”——图片区(Stack 布局)

“轮子”有了,开始画它的 build() 方法。一个卡片得有“颜值”担当,我们先用 Stack(堆叠)布局,把图片作为“背景板”。
// 这是 buildNewsCard 组件里的 build() 方法
build() {Column({ space: 12 }) { // 用一个 Column 把图片和文字包起来// 图片区域Stack() {// 网络图片Image(this.localNews.imageUrl).width('100%').height(200).objectFit(ImageFit.Cover) // 让图片不变形地填满,裁掉多余的.borderRadius(12).alt($r('app.media.avatar')) // 翻车(加载失败)时的占位图// 分类标签Text(this.localNews.category).fontSize(12).fontColor('#FFFFFF').backgroundColor('#007DFF').borderRadius(4).padding({ left: 8, right: 8, top: 4, bottom: 4 }).position({ x: 12, y: 12 }) // 精准“贴”在左上角}// ... 内容区(下一步) ...}.backgroundColor('#FFFFFF').borderRadius(12) // 整个卡片的背景和圆角
}
笔记:
Stack 布局就是“叠叠乐”,后写的会盖在先写的上面。我们用 position 把“分类标签”精准地“贴”在左上角。objectFit(ImageFit.Cover) 是个神技,能让各种比例的图片都好看!
第四步:卡片“灵魂”——内容区(Column + Row)

光有图不行,得有“灵魂”——标题和摘要。这部分紧跟在 Stack 后面,放在外层的 Column 里。
// ... Stack 布局结束 ...// 内容区域Column({ space: 12 }) {// 标题区域Text(this.localNews.title).fontSize(18).fontWeight(FontWeight.Medium).fontColor('#1A1A1A').maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出2行变“...”// 摘要区域Text(this.localNews.summary).fontSize(14).fontColor('#666666').maxLines(3).textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出3行变“...”.lineHeight(20)// ... 底部信息栏(下一步) ...}.padding({ left: 16, right: 16, bottom: 16 }) // 给内容区加点内边距
笔记:
这部分全是老朋友:fontSize、fontWeight、maxLines(最多两行)、textOverflow(超出部分变“…”)。这套“组合拳”打出去,UI 一下子就清爽了。
第四步(续):卡片的“脚”——底部信息栏(交互核心)

卡片快完工了,还差个“脚”——底部信息栏。一个 Row(水平布局)搞定,左边放时间和阅读量,右边放点赞。
// ... 摘要区域结束 ...// 底部信息栏Row() {// 左侧信息(发布时间、阅读量)Column({ space: 2 }) {Row({ space: 8 }) {Image($r('app.media.shijian')).width(18).height(18)Text(this.localNews.publishTime).fontSize(12).fontColor('#999999')}Row({ space: 8 }) {Image($r('app.media.yuedu')).width(18).height(18)Text(`${this.localNews.readCount}`).fontSize(12).fontColor('#999999')}}.layoutWeight(1) // 自动“抢”走所有剩余空间,把点赞按钮挤到最右边// 点赞按钮Row({ space: 6 }) {Image(this.localNews.isLiked ? $r('app.media.dianzan2') : $r('app.media.dianzan')).width(30).height(30)Text(this.localNews.isLiked ? '已赞' : '点赞').fontSize(12).fontColor(this.localNews.isLiked ? '#007DFF' : '#999999')}.onClick(() => { // 点击时,“反转”自己的内部状态this.localNews.isLiked = !this.localNews.isLiked;}).backgroundColor(this.localNews.isLiked ? '#E6F2FF' : '#F5F5F5').borderRadius(6).padding({left: 8, right: 8, top: 4, bottom: 4}) // 给点赞按钮加点内边距}} // 内容区的 Column 结束} // 整个卡片的 Column 结束
} // build() 方法结束
笔记:
layoutWeight(1) 又是神技!它让左边的信息栏“霸道”地占据所有剩余空间,从而把点赞按钮“挤”到最右边,完美实现两端对齐!
注意看“点赞”按钮!我们用了一个“三元运算符” (this.localNews.isLiked ? ... : ...)。如果 isLiked 是 true,就显示“已赞”图标和蓝色;如果是 false,就显示“点赞”图标和灰色。连背景色都换了!
onClick 时,我们只修改了 this.localNews 这个“内部状态”,这就是 @Component + @State 的威力!
第五步:“组装” App!——主页面架构

“零件”造好了,现在开始“组装”我们的 App 主页面!@Entry 登场!
@Entry
@Component
export struct NewsCardDemo {// 伪造一堆新闻数据,记得用你刚才定义的 NewsItem 类@State newsList: NewsItem[] = [new NewsItem(1, '鸿蒙新版发布!', '性能提升30%...', 'app.media.hm', '科技', '1小时前', 1024, false),new NewsItem(2, 'xx明星演唱会', '现场燃爆...', 'app.media.star', '娱乐', '2小时前', 5890, true),new NewsItem(3, '国足又...进球了!', '球员xx梅开二度...', 'app.media.gz', '体育', '3小时前', 888, false)// ... 伪造更多数据 ...];@State selectedCategory: string = '全部'; // 用@State管理当前选中的分类categories: string[] = ['全部', '科技', '娱乐', '体育', '财经'];build() {Column({ space: 0 }) {// 顶部标题栏(这里我偷懒,你也可以用@Builder写一个)Text('新闻资讯').fontSize(20).fontWeight(FontWeight.Bold).padding(16).width('100%')// 分类筛选this.buildCategoryFilter()// 新闻列表this.buildNewsList()}.width('100%').height('100%').backgroundColor('#F5F5F5')}// ... buildCategoryFilter 和 buildNewsList 马上就来 ...
}
笔记:
这个主页面也用 @State 来管理“全局”的新闻列表 newsList 和当前选中的分类 selectedCategory。build 方法里,我们用 Column 把页面分成了“上(标题)”、“中(筛选)”、“下(列表)”三个部分。
第六步:实现“横向滚动”的分类筛选条

分类筛选条是个高频需求。用 Scroll 包一个 Row,就能让它“横向滚动”。
// 在 NewsCardDemo struct 内部
@Builder
buildCategoryFilter() {Scroll() { // 横向滚动容器Row({ space: 12 }) {ForEach(this.categories, (category: string) => {Text(category).fontSize(16).fontColor(this.selectedCategory === category ? '#FFFFFF' : '#666666') // 选中高亮.backgroundColor(this.selectedCategory === category ? '#007DFF' : '#F0F0F0') // 选中高亮.borderRadius(20).padding({left: 16, right: 16, top: 8, bottom: 8}).onClick(() => { // 关键:点击时,更新@State变量this.selectedCategory = category; })})}.padding({ left: 16, right: 16, top: 12, bottom: 12 })}.scrollBar(BarState.Off) // 关掉丑丑的滚动条
}
笔记:
又是“三元运算符”的胜利!this.selectedCategory === category ? ... : ...。
最最关键的是 onClick:点击谁,谁就高亮,同时把 @State 变量 this.selectedCategory 的值改成被点击的 category。
这个 @State 一变,奇迹发生了…(请看下一步)
第七步:“魔法”发生地!—— 响应式新闻列表

奇迹来了!还记得吗?ArkTS 是“响应式”的!当你点击分类条,@State 的 selectedCategory 一变,所有“依赖”了它的 UI 都会“自动”重新渲染!
buildNewsList 就“依赖”了它!
// 在 NewsCardDemo struct 内部
@Builder
buildNewsList() {List({ space: 12 }) {// 魔法!在这里用 filter 过滤数据!ForEach(this.newsList.filter(item =>// 如果选的是“全部”,或者 item 的分类 匹配 选中的分类this.selectedCategory === '全部' || item.category === this.selectedCategory), (news: NewsItem) => {ListItem() {// 调用我们刚才辛辛苦苦“造的轮子”buildNewsCard({ news: news })}}, (news: NewsItem) => news.id.toString()) // 用 id 作为唯一标识,提升性能}.width('100%').layoutWeight(1) // 占满剩余所有空间.padding({ left: 16, right: 16, top: 12, bottom: 12 })
}
笔记:
看 ForEach 里的 this.newsList.filter(...)!这就是“魔法”核心!
filter 方法会根据 this.selectedCategory 筛选出“应该”显示的新闻。
整个流程是:
- 你点击“科技”按钮。
onClick把@State变量selectedCategory改成了 “科技”。- ArkTS 框架发现
@State变了,马上“通知”所有用到它的地方。 buildNewsList重新执行build()。ForEach里的filter重新计算,这次只返回了category === '科技'的新闻。List自动更新!
丝滑!这就是“数据驱动 UI”的魅力!
😱 “萌新”踩坑(含泪)实录
“常在河边走,哪能不湿鞋”。下面是我(含泪)总结的“踩坑”经验,各位“萌新”请拿好,可以少走几公里弯路!
巨坑 1:我改了“爹”的数据!(@Prop vs @State)
- 现象: 我一开始在子组件
buildNewsCard里,想点赞时直接改this.news.isLiked… 结果,卒!要么不生效,要么“污染”了父组件。 - 血泪教训:
@Prop是“爹”给的,是只读的! 你不能在“儿子”组件里直接改“爹”的数据! - 正解: 就像我们前面做的,
aboutToAppear里把@Prop的news复制给@State的localNews。组件内部只修改localNews,实现“自给自足”。
// 正确做法:使用内部状态管理
@State localNews: NewsItem = new NewsItem(0, '', '', '', '', '', 0, false)aboutToAppear() {if (this.news) {this.localNews = new NewsItem(/* 复制数据 */);}
}
小坑 2:图片加载“翻车”
- 现象: 网速不好时,图片加载失败,卡片上出现一个大“窟窿”,丑爆了。
- 正解:
Image组件有两个好基友.alt()和.onError()。
Image(this.localNews.imageUrl).alt($r('app.media.avatar')) // 加载失败/加载中 显示的占位图.onError(() => {console.log(`图片加载失败: ${this.localNews.title}`);})
小坑 3:布局“偏心眼”
- 现象: 底部信息栏,左边的日期和右边的点赞,挤在一起了,或者对不齐。
- 正解: 善用
layoutWeight(1)!给那个你希望它“尽可能伸展”的组件(比如我们左边的信息栏Column)加上它,它就会自动“抢”走所有剩余空间。
Column({ space: 2 }) {// 左侧信息内容
}
.layoutWeight(1) // 占据剩余空间
“精装修”建议(下一步玩啥)
这个 Demo 只是个“毛坯房”,想“精装修”?你可以试试:
- 搜索功能:在标题栏下面加个搜索框,用
filter按“标题” (title.includes(searchText)) 搜索。 - 详情页面:点击卡片,跳转到完整的新闻详情页(
Navigation组件该出场了)。 - 下拉刷新:给
List加上下拉刷新功能,更新newsList里的数据。 - 上拉加载:滚动到底部时,自动加载“下一页”数据(
onReachEnd事件)。
总结:你又“行”了!
呼——(擦汗)。这个项目“肝”下来,是不是感觉自己又“行”了?

我们从一个“空架子”开始,亲手“锻造”了数据模型,“封装”了高复用性的 @Component 卡片(还搞懂了 @Prop 和 @State 的“父子关系”),用 Stack 玩转了“叠罗汉”布局,最后用 @State 和 filter 联手实现了“响应式”筛选列表。
这已经是一个准专业 App 的雏形了!
从“国风” Demo 到这个新闻 App,你已经从“新手村”毕业,拿到了“高级组件”的徽章。继续保持这份热情,下一个项目,我们去挑战更酷的!
