纯血鸿蒙 ArkTS 开发实战:Grid 与 List 组合构建高颜值编程练习应用
纯血鸿蒙 ArkTS 开发实战:Grid 与 List 组合构建高颜值编程练习应用
基于 HarmonyOS 5.0(API 12)+ DevEco Studio 5.0,完整复盘一个真实可运行的 ArkTS Demo,从零搭建类 LeetCode 的"编程练习"页面。涉及声明式 UI、状态管理、组件通信、样式调优等核心知识点,适合鸿蒙原生开发新手精读。
运行截图:




一、前言
2023 年 HarmonyOS NEXT(俗称"纯血鸿蒙")发布,系统底座、AOSP 代码全部移除,真正做到自主可控。与之配套的 ArkTS,在 TypeScript 语法基础上通过 @State、@Prop、@Link 等装饰器实现细粒度响应式 UI,配合方舟编译器的统一运行时,带来媲美原生的高性能体验。
本文从 0 到 1 实战一个编程练习列表页:用 Grid 展示语言选择标签(Java/Python/TS/C++),用 List 展示题目卡片,点击语言切换时列表自动过滤。文末分享几个我踩过的坑。
二、效果速览
页面三层结构:
- 顶部标题栏:白底,"编程练习"四个大字醒目。
- 语言选择 Grid:5 个胶囊标签横向排开,选中态蓝色实心,未选中态灰色描边。
- 题目列表 List:每张卡片显示题目标题、难度徽章、题目描述(最多两行省略)、语言徽章 + 知识点标签。
切换语言时,Grid 高亮会移动,List 同步过滤对应语言的题目,空集时显示"暂无该语言的题目"占位。
三、项目搭建
本项目基于官方 Application 模板,核心目录:
entry/src/main/ets/
├── entryability/EntryAbility.ets # UIAbility 入口
└── pages/Index.ets # 主页(本文重点)
DevEco Studio 创建项目后,只需修改 entry/src/main/ets/pages/Index.ets 一个文件即可完成全部 UI 逻辑。main_pages.json 默认已注册 pages/Index,无需调整。
推荐把每个独立功能拆成独立
.ets文件,如components/LanguageGrid.ets、components/QuestionItem.ets,便于维护复用。本 Demo 出于演示目的,代码集中在单文件。
四、数据模型设计
Index.ets 底部定义 QuestionItem 接口:
interface QuestionItem {
id: number;
title: string;
language: string;
difficulty: string;
tags: string[];
desc: string;
}
字段说明:id 用于 ForEach 的 key;language 按语言过滤;difficulty 决定徽章颜色(简单=绿、中等=橙、困难=红);tags 知识点标签;desc 题目描述,UI 上限 2 行。
组件内定义 12 条题目数据,覆盖四种语言:
private allQuestions: QuestionItem[] = [
{ id: 1, title: '两数之和', language: 'Java', difficulty: '简单', tags: ['数组', '哈希表'], desc: '给定一个整数数组和一个目标值,找出和为目标值的两个整数并返回下标。' },
{ id: 2, title: '最长回文子串', language: 'Java', difficulty: '中等', tags: ['字符串', '动态规划'], desc: '给你一个字符串 s,找到 s 中最长的回文子串。' },
{ id: 3, title: '二叉树的最大深度', language: 'Java', difficulty: '简单', tags: ['树', 'DFS'], desc: '给定一个二叉树,找出其最大深度。' },
{ id: 4, title: '反转链表', language: 'Java', difficulty: '简单', tags: ['链表', '递归'], desc: '反转一个单链表。' },
{ id: 5, title: 'LRU 缓存机制', language: 'Java', difficulty: '困难', tags: ['设计', '哈希表', '链表'], desc: '设计并实现一个 LRU(最近最少使用)缓存机制。' },
{ id: 6, title: '有效的括号', language: 'Python', difficulty: '简单', tags: ['栈', '字符串'], desc: '给定一个只包括 "()[]{}" 的字符串,判断字符串是否有效。' },
{ id: 7, title: '两数相加', language: 'Python', difficulty: '中等', tags: ['链表', '数学'], desc: '给你两个非空链表,表示两个非负整数,将它们相加并以链表形式返回。' },
{ id: 8, title: '合并有序数组', language: 'TS', difficulty: '简单', tags: ['数组', '双指针'], desc: '将两个有序数组合并为一个有序数组。' },
{ id: 9, title: '最小路径和', language: 'C++', difficulty: '中等', tags: ['动态规划', '网格'], desc: '给定一个 m x n 的网格,找出从左上角到右下角的最小路径和。' },
{ id: 10, title: '岛屿数量', language: 'C++', difficulty: '中等', tags: ['DFS', 'BFS', '图'], desc: '计算二维网格中岛屿的数量。' },
{ id: 11, title: '最长递增子序列', language: 'TS', difficulty: '中等', tags: ['动态规划', '二分'], desc: '找到给定数组中最长递增子序列的长度。' },
{ id: 12, title: '爬楼梯', language: 'Python', difficulty: '简单', tags: ['动态规划'], desc: '假设你正在爬楼梯,需要 n 阶到达顶部,每次可以爬 1 或 2 个台阶。' }
];
真实业务中,数据一般通过 @ohos.net.http 或 @ohos.data.preferences 从远端/本地加载。
五、Grid 组件实现语言选择标签
Grid 是 ArkUI 中最常用的二维布局组件之一。这里让 5 个标签横向排成 1 行。
5.1 列模板与尺寸
Grid() {
ForEach(this.languages, (lang: string) => {
GridItem() { Text(lang) }
.height(40)
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
.width('100%')
.height(80)
.backgroundColor('#FFFFFF')
columnsTemplate('1fr 1fr 1fr 1fr 1fr'):5 列等宽,1fr是分数单位。columnsGap(10):列间距。height(80):固定高度,避免布局抖动。
5.2 选中态样式
用三元表达式根据 selectedLanguage 切换样式:
Text(lang)
.fontSize(15)
.fontWeight(this.selectedLanguage === lang ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.selectedLanguage === lang ? '#FFFFFF' : '#182431')
.width('100%').height('100%')
.textAlign(TextAlign.Center)
.borderRadius(20)
.backgroundColor(this.selectedLanguage === lang ? '#007DFF' : '#F2F3F5')
.onClick(() => { this.selectedLanguage = lang; })
- 未选中:
#F2F3F5灰底 +#182431深色字。 - 选中:
#007DFF蓝底 + 白字 + Bold,主色突出。 borderRadius(20):胶囊圆角,半高 = 高度 40 一半,完美。
5.3 点击事件:挂哪一层很关键
结论:推荐挂在内层
Text上。
在 HarmonyOS 4.0 之前,GridItem 默认不接收点击事件,需先 .hitTestBehavior(HitTestMode.Transparent) 显式声明。把事件挂到 Text 上是最稳妥的写法,跨版本兼容性最好。
点击后,@State 装饰的 selectedLanguage 被赋值,ArkUI 框架自动重渲染整个 build 树——这就是 ArkTS 声明式 UI + 响应式状态 的精髓。
六、List 组件实现题目列表
List 是 ArkUI 中负责长列表渲染的容器,内部实现懒加载,可轻松承载上万条数据,性能远超 ForEach + Column。
6.1 List 整体结构
List() {
ListItem() { /* 头部统计信息 */ }
if (this.getQuestions().length === 0) {
ListItem() { /* 空状态占位 */ }
} else {
ForEach(this.getQuestions(), (item: QuestionItem) => {
ListItem() { /* 题目卡片 */ }
})
}
ListItem() { /* 底部"已经到底啦" */ }
}
.layoutWeight(1)
.backgroundColor('#F5F6F8')
layoutWeight(1):在Column中自动撑满剩余空间,这是 ArkUI 实现"头部固定、列表区滚动"的标准写法。ListItem必须直接是List的子组件,不能套Column,否则报错。- 列表内的
if/else是 ArkTS 提供的条件渲染。
6.2 题目卡片样式
每张卡片是一个白底、圆角的 Column:
Column() {
Row() {
Text(item.title).fontSize(16).fontWeight(FontWeight.Medium).fontColor('#182431')
Blank()
Text(item.difficulty)
.fontSize(12)
.fontColor(this.difficultyColor(item.difficulty))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(10)
.backgroundColor(this.difficultyColor(item.difficulty) + '22')
}
.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(13).fontColor('#5F6B7C')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 }).width('100%')
Row() {
Text(item.language)
.fontSize(11).fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10).backgroundColor('#007DFF').margin({ right: 6 })
ForEach(item.tags, (tag: string) => {
Text(tag)
.fontSize(11).fontColor('#007DFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10).backgroundColor('#E8F3FF').margin({ right: 6 })
})
}
.width('100%').margin({ top: 8 })
}
.padding(14).alignItems(HorizontalAlign.Start)
.backgroundColor('#FFFFFF').borderRadius(10)
.margin({ left: 12, right: 12, top: 6, bottom: 6 })
- 第一行
Row:左边标题、右边难度徽章,Blank()等价于flex: 1。 - 描述:
maxLines(2) + textOverflow({ overflow: TextOverflow.Ellipsis })实现两行省略。 - 标签行:语言徽章蓝底白字(主色),知识点标签蓝字浅蓝底(辅助色)。
- 卡片外层
margin与 List 的灰底配合,模拟"卡片浮在灰底上"的 Material Design 风格。
6.3 难度颜色:封装方法
difficultyColor(difficulty: string): string {
if (difficulty === '简单') return '#52C41A';
if (difficulty === '中等') return '#FAAD14';
return '#FF4D4F';
}
颜色技巧:为让徽章背景柔和,用 '#52C41A' + '22' 拼接 8 位色值,后两位 22 是 alpha 通道(≈13% 透明度),相当于"主色 + 13% 不透明"。这种"用透明度而非浅色"的做法可保持色调一致。
6.4 过滤逻辑
getQuestions(): QuestionItem[] {
if (this.selectedLanguage === '全部') return this.allQuestions;
return this.allQuestions.filter((q: QuestionItem) => q.language === this.selectedLanguage);
}
把过滤逻辑封装在方法里,两个好处:头部统计"共 N 题"和列表渲染共用,避免不一致;未来扩展"按难度过滤"等只需叠加 filter。
七、状态管理与响应式联动
ArkTS 装饰器语义清晰:
@State:组件内部状态,变化触发自身重 build。@Prop:父组件传入的单向同步状态。@Link:父子双向同步状态。@Provide/@Consume:跨层级状态共享。@Observed/@ObjectLink:监听对象/数组深层变化。
本 Demo 只用 @State,因所有交互都发生在当前组件内:
@State selectedLanguage: string = '全部';
7.1 响应式数据流
用户点击 Text
↓
this.selectedLanguage = lang
↓
ArkUI 框架捕获状态变化
↓
触发 build() 重执行
↓
getQuestions() 用新 selectedLanguage 计算
↓
ForEach diff 更新
↓
用户看到 List 内容刷新
整个过程无需手动 setState / forceUpdate。声明式 UI 的最大优势:开发者只关心"状态长什么样",不关心"如何更新到 UI"。
7.2 为什么不用普通变量?
private selectedLanguage: string = '全部'; // 错误示范
private 虽能存值,但不会触发 UI 更新。ArkUI 通过装饰器的 getter/setter 拦截赋值,普通变量没有这层拦截。
一句话:在 ArkUI 中,只有被装饰器修饰的变量才"会动"。
八、踩坑记录
坑 1:ForEach 中调用 getter 性能问题。 每次 build 都会重新 filter。简单场景无影响,大数据场景应改用 @State 手动维护过滤数组。
坑 2:GridItem 的 onClick 不响应。 GridItem 默认对点击事件"免疫",需把 .onClick 挂到内部 Text 上。
坑 3:ListItem 不能套 Column。 List() { Column() { ForEach(...) } } 直接报错 ListItem can only be a direct child of List,ListItem 必须是 List 的直接子组件。
坑 4:文字溢出没省略号。 只设 maxLines(2) 但没设 textOverflow,文字会被截断但不显示省略号,两者必须同时配置。
坑 5:Blank() 在 ListItem 内失效。 Blank 只在 Flex/Row 主轴方向生效,父容器是 ListItem 时需显式 flexGrow(1) 替代。
九、UI 调优细节
代码能跑不等于"好看"。以下细节让 Demo 摆脱"学生作品"标签。
9.1 颜色体系
- 主色
#007DFF:按钮、徽章实心,代表主操作。 - 辅色
#E8F3FF:徽章背景,呼应主色但视觉重量轻。 - 文字主色
#182431:接近纯黑但带蓝调,比纯黑柔和。 - 文字次色
#5F6B7C:描述用,拉开层次。 - 文字辅助色
#8A8F99:统计信息,最弱。 - 背景灰
#F5F6F8:页面底色,比纯白稍灰,卡片"浮"起来更立体。
9.2 间距节奏
- 卡片之间:垂直 6px + 水平 12px + 内 padding 14px,总约 26px 呼吸感。
- 卡片内分区:标题→描述 6px,描述→标签 8px,标题与难度徽章同行 0px。
- 整体 padding:左右统一 16px(标题栏)、12px(列表),形成视觉对齐。
9.3 字体层级
- 一级(标题):24px Bold
- 二级(题目):16px Medium
- 三级(描述):13px Regular
- 四级(徽章):12px Medium / 11px Regular
- 五级(统计):13px Regular 灰
通过 fontSize + fontWeight + fontColor 三维组合,让信息"一眼扫到重点"。
9.4 圆角一致。整页圆角均为 10(小标签)或 20(Grid 胶囊),形成规律。
十、性能优化与最佳实践
-
ForEach 一定要传 key:
ForEach(arr, fn, (item) => item.id.toString()),告诉框架用 id 区分每项,避免 List 全部重建。 -
避免在 build 中做重计算:大数据场景下把过滤结果存到
@State,只在筛选条件变化时更新。 -
复用子组件:题目卡片可抽成
@Component struct QuestionCard,既能减少代码,又能让 ArkUI 复用组件实例,提升性能。 -
合理使用 cachedCount:List 默认预加载屏外 1 个 ListItem,卡片复杂可调大,简单可调小。
-
善用 if 条件渲染:用
if (this.list.length === 0)直接控制空状态,完全脱离渲染树,既省内存又触发 List 重新计算布局。
十一、可扩展方向
- 远端数据接入:用
@ohos.net.http请求 LeetCode 公开 API,加载真实题库。 - 搜索/筛选:顶部加
Search组件,支持按标题/标签实时过滤。 - 题目详情页:点击 ListItem 跳转新页面,展示完整描述、代码编辑器、提交按钮。
- 收藏功能:用
@ohos.data.preferences持久化收藏的题目 ID。 - 暗黑模式:在
resources/dark/element/color.json提供暗黑色板。 - 多语言国际化:抽取文案到
string.json,支持中/英切换。
十二、写在最后
HarmonyOS NEXT 已正式商用,鸿蒙开发岗位需求在 2024-2025 年呈"井喷"态势。无论是转型 Android/iOS 的工程师,还是入行前端/全栈的新人,ArkTS 都是值得投入学习的方向。
本文虽只是一个编程练习页,但涵盖了 ArkTS 开发的核心套路:状态装饰器 + 响应式 UI、Grid/List 声明式布局、样式与组件的模块化、性能与最佳实践。
希望这篇文章能帮你少走弯路。如果觉得有帮助,欢迎点赞、收藏、转发,你的支持是我持续创作的最大动力!
创作时间:2026 年 | 项目源码:见本仓库根目录
更多推荐



所有评论(0)