纯血鸿蒙 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 展示题目卡片,点击语言切换时列表自动过滤。文末分享几个我踩过的坑。


二、效果速览

页面三层结构:

  1. 顶部标题栏:白底,"编程练习"四个大字醒目。
  2. 语言选择 Grid:5 个胶囊标签横向排开,选中态蓝色实心,未选中态灰色描边。
  3. 题目列表 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.etscomponents/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 胶囊),形成规律。


十、性能优化与最佳实践

  1. ForEach 一定要传 key:ForEach(arr, fn, (item) => item.id.toString()),告诉框架用 id 区分每项,避免 List 全部重建。

  2. 避免在 build 中做重计算:大数据场景下把过滤结果存到 @State,只在筛选条件变化时更新。

  3. 复用子组件:题目卡片可抽成 @Component struct QuestionCard,既能减少代码,又能让 ArkUI 复用组件实例,提升性能。

  4. 合理使用 cachedCount:List 默认预加载屏外 1 个 ListItem,卡片复杂可调大,简单可调小。

  5. 善用 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 年 | 项目源码:见本仓库根目录

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐