鸿蒙原生 ArkTS 布局方式之 List 分组(Group):带标题的分组列表 —— 通讯录风格实现详解
一、引言
在移动端应用开发中,分组列表(Section List)是最常见的 UI 模式之一。从通讯录到商品分类页,从设置菜单到好友列表,分组列表几乎无处不在。其核心特征是将数据按首字母、时间或类别等维度分组展示,每一组拥有一个可吸顶的标题头,帮助用户快速定位。
HarmonyOS NEXT 的原生 UI 框架ArkUI提供了声明式的 ArkTS 语言布局体系。其中,List容器配合ListItemGroup子容器构成了实现分组列表的标准方案,官方将其归纳为「List 分组(Group):带标题的分组列表」,是 ArkTS 六大基础布局之一。
本文将围绕一个通讯录风格的分组列表示例,从数据模型、界面布局到完整代码,逐层剖析这一布局的实现原理。
二、HarmonyOS NEXT 与 ArkTS 布局体系概述
2.1 从 Java 到 ArkTS 的演进
HarmonyOS 的应用开发语言经历了从 Java/JS 双框架(1.0–2.0)到 eTS(3.0),再到 HarmonyOS NEXT(API 11+)全面采用ArkTS的演进。ArkTS 是 TypeScript 的超集,增加了@Component / @Entry / @Builder / @State等装饰器体系,实现声明式 + 数据驱动的 UI 开发范式。
2.2 六大基础布局方式
ArkUI 官方归纳六种核心布局,覆盖绝大多数应用场景:线性布局(Row/Column)、层叠布局(Stack)、弹性布局(Flex)、相对布局(RelativeContainer)、滚动布局(Scroll)、列表布局(List)和网格布局(Grid/GridItem)。本文聚焦的正是列表布局的高阶用法——带标题的分组列表(ListItemGroup)。
2.3 为什么选择 ListItemGroup?
在 iOS 开发中,UITableView通过section概念天然支持分组;在 Android 中,RecyclerView配合ItemDecoration可以实现分组效果。而在 ArkUI 中,ListItemGroup就是实现分组语义的第一等公民。
与直接使用多个List拼接或用Divider手动模拟分组相比,ListItemGroup的优势:
- 语义明确:代码结构天然反映
List > ListItemGroup > ListItem层级; - 标题吸顶:通过
sticky属性一键开启,无需手动计算滚动偏移; - 性能优化:继承
List的懒加载能力,不会一次性渲染所有分组; - 交互丰富:支持侧滑删除(swipeAction)、拖拽排序等高级交互。
三、示例应用概览
3.1 最终效果
模拟手机通讯录:顶部标题栏「通讯录」,列表按首字母分组(★ 置顶 + A~Z),每组上方有灰色标题栏,滚动时标题吸顶固定。每个联系人条目包含圆形头像(首字+随机色)、姓名、手机号、拨号图标。点击联系人弹出 Toast 提示。
3.2 数据模型
interfaceContactInfo{name:string;phone:string;}interfaceGroupData{title:string;contacts:ContactInfo[];}整个列表的数据源是一个GroupData[]数组。本示例定义了10 个分组、28 个联系人,涵盖 ★(置顶)、A、B、C、D、G、H、L、W、Z 等字母,模拟真实通讯录中「并非每个字母都有联系人」的情况。
3.3 组件树结构
Column ← 页面根容器 ├── Row ← 顶部标题栏 └── List ← 滚动列表(sticky header) ├── ListItemGroup (A) ← 分组 A(header: "A") │ ├── ListItem ← 联系人「阿杰」 │ ├── ListItem ← 联系人「艾米」 │ └── ListItem ← 联系人「安琪拉」 ├── ListItemGroup (B) ← 分组 B └── ListItemGroup (Z) ← 分组 Z这个层级清晰映射了「列表 → 多个分组 → 每个分组下多个条目」的数据关系。
四、核心代码逐段解析
4.1 导入语句与数据接口
import{promptAction}from'@kit.ArkUI';interfaceContactInfo{name:string;phone:string;}interfaceGroupData{title:string;contacts:ContactInfo[];}@kit.ArkUI是 HarmonyOS NEXT 的统一 SDK 包入口,替代了旧版本的多个分散导入(如@ohos.promptAction)。promptAction提供了 Toast 弹窗等轻量交互能力。
ContactInfo和GroupData两个接口是数据驱动的基石。在真实工程中,这些接口通常由后端 API 返回的数据结构定义,或由数据库 ORM 映射而来。
4.2 @State 数据源与 @Builder 头像
@StategroupDataList:GroupData[]=[/* 10 个分组、28 个联系人 */];@BuildercontactAvatar(name:string){Text(name.charAt(0)).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Bold).width(44).height(44).borderRadius(22).backgroundColor(this.getAvatarColor(name)).textAlign(TextAlign.Center)}@State装饰器是 ArkTS 状态管理的核心——被@State修饰的变量变化时,框架自动重渲染相关组件。@Builder装饰的方法是一个可复用的 UI 片段构建器,支持参数传递。此处构建了 44×44 的圆形头像,首字白色粗体居中,背景色根据姓名 Unicode 哈希取模从 10 种预设颜色中选取。
4.3 Build 方法:核心布局逻辑
build(){Column(){Row(){Text('通讯录')...}// 顶部标题List({space:0}){ForEach(this.groupDataList,(groupItem)=>{ListItemGroup({header:this.groupHeaderBuilder(groupItem.title)}){ForEach(groupItem.contacts,(contact)=>{ListItem(){Row(){this.contactAvatar(contact.name)Column(){Text(contact.name)...Text(contact.phone)...}Image($r('app.media.startIcon'))...}}})}})}.sticky(StickyStyle.Header)// 吸顶效果}}核心是双重 ForEach 循环:外层遍历groupDataList生成ListItemGroup,内层遍历groupItem.contacts生成ListItem。ForEach的第三个参数是键值生成函数,用于框架追踪列表项变化。
4.4 吸顶属性与交互
List({space:0}).sticky(StickyStyle.Header)StickyStyle枚举有三个值:None(不吸顶)、Header(标题吸顶)、Footer(底部吸底)。设置Header后,ArkUI 自动处理所有ListItemGroupheader 的位置计算。
点击交互使用promptAction.showToast实现 Toast 提示:
onContactClick(contact:ContactInfo):void{promptAction.showToast({message:`拨打电话:${contact.name}(${contact.phone})`,duration:2000});}五、ListItemGroup 的关键属性详解
5.1 header 属性
header是ListItemGroup最重要的属性,它接收一个@Builder或自定义构建器,用于渲染分组的标题头。
ListItemGroup({header:this.groupHeaderBuilder(groupItem.title)}){// 分组内的列表项}groupHeaderBuilder的实现(36px 高、浅灰背景的文字区域):
5.2 divider 属性
.divider({strokeWidth:0,startMargin:0,endMargin:0})divider控制分组之间的分隔线样式。本示例中隐藏了组间分割线,而每个ListItem内部通过.border设置 0.5 像素的底部分割线,形成「组内条目有分割线、组间无额外分割线」的效果。
5.3 children(插槽内容)
ListItemGroup的花括号内容只能放置ListItem组件(或ForEach/LazyForEach生成的ListItem)。
六、BorderOptions 在 API 24 中的正确使用
在 API 24 中,BorderOptions接口的定义发生了变化——不能再将bottom、left等边属性直接放在BorderOptions顶层,而需嵌套在width和color内部:
// ❌ 编译错误:'bottom' does not exist in type 'BorderOptions'.border({bottom:{width:0.5,color:'#E8E8E8'}})// ✅ 正确写法.border({width:{bottom:0.5},color:{bottom:'#E8E8E8'}})BorderOptions的类型定义(简化):
interfaceBorderOptions{width?:EdgeWidths|Dimension;// EdgeWidths = { left?, right?, top?, bottom? }color?:EdgeColors|ResourceColor;// EdgeColors = { left?, right?, top?, bottom? }style?:EdgeStyles|BorderStyle;radius?:BorderRadiuses|Dimension;}设置单边边框用{ width: { bottom: 1 }, color: { bottom: '#ccc' } },设置四边统一边框用{ width: 1, color: '#ccc' },设置各边不同边框用{ width: { left: 1, right: 2 }, color: { left: 'red', right: 'blue' } }。
七、性能考量:ForEach 与 LazyForEach
本示例使用ForEach,它会一次性渲染所有 ListItem。数据量较少时没有问题。当数据量达数百或数千时,应使用LazyForEach替代:
List(){LazyForEach(this.dataSource,(item:GroupData)=>{ListItemGroup({header:...}){LazyForEach(item.contacts,(contact:ContactInfo)=>{ListItem(){...}},(contact)=>contact.phone)}},(item)=>item.title)}.sticky(StickyStyle.Header)LazyForEach需配合IDataSource接口使用,按需创建、回收复用,大幅降低内存占用。
八、扩展建议
- 字母索引条:使用
AlphabetIndexer组件配合ListController.scrollToIndex实现右侧字母快速跳转 - 侧滑操作:
ListItem的swipeAction属性支持添加侧滑删除/置顶菜单 - 数据持久化:实际工程中建议使用
LazyForEach+ 数据源接口,从RelationalStore或云端获取数据
九、完整代码清单
以下为
ContactGroupList.ets的完整代码(已包含详尽的中文注释),完整文件位于entry/src/main/ets/pages/ContactGroupList.ets。
import{promptAction}from'@kit.ArkUI';interfaceContactInfo{name:string;phone:string;}interfaceGroupData{title:string;contacts:ContactInfo[];}@Entry@Componentstruct ContactGroupList{@StategroupDataList:GroupData[]=[{title:'★',contacts:[{name:'张三',phone:'138****1234'},{name:'李四',phone:'139****5678'}]},{title:'A',contacts:[{name:'阿杰',phone:'136****1111'},{name:'艾米',phone:'137****2222'},{name:'安琪拉',phone:'135****3333'}]},{title:'B',contacts:[{name:'白杨',phone:'150****4444'},{name:'本杰明',phone:'151****5555'}]},{title:'C',contacts:[{name:'陈晨',phone:'152****6666'},{name:'程菲',phone:'153****7777'},{name:'柴远',phone:'155****8888'}]},{title:'D',contacts:[{name:'邓超',phone:'156****9999'},{name:'董洁',phone:'157****0000'}]},{title:'G',contacts:[{name:'高天',phone:'158****1111'},{name:'郭靖',phone:'159****2222'}]},{title:'H',contacts:[{name:'韩梅梅',phone:'170****3333'},{name:'何炅',phone:'171****4444'}]},{title:'L',contacts:[{name:'李华',phone:'172****5555'},{name:'刘洋',phone:'173****6666'},{name:'林娜',phone:'174****7777'}]},{title:'W',contacts:[{name:'王伟',phone:'175****8888'},{name:'吴芳',phone:'176****9999'},{name:'魏明',phone:'177****0000'}]},{title:'Z',contacts:[{name:'张宇',phone:'178****1111'},{name:'赵敏',phone:'179****2222'},{name:'周杰',phone:'180****3333'}]}];onContactClick(contact:ContactInfo):void{promptAction.showToast({message:`拨打电话:${contact.name}(${contact.phone})`,duration:2000});}@BuildercontactAvatar(name:string){Text(name.charAt(0)).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Bold).width(44).height(44).borderRadius(22).backgroundColor(this.getAvatarColor(name)).textAlign(TextAlign.Center)}getAvatarColor(name:string):ResourceColor{constcolors:ResourceColor[]=['#FF6B81','#5B8FF9','#F6BD16','#E8684A','#2FC25B','#9F7EEA','#F4606C','#20B2AA','#FF7F50','#9370DB'];letindex=0;for(leti=0;i<name.length;i++)index=(index+name.charCodeAt(i))%colors.length;returncolors[index];}build(){Column(){Row(){Text('通讯录').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#333333')}.width('100%').padding({left:16,top:12,bottom:8}).backgroundColor('#F7F7F7')List({space:0}){ForEach(this.groupDataList,(groupItem:GroupData)=>{ListItemGroup({header:this.groupHeaderBuilder(groupItem.title)}){ForEach(groupItem.contacts,(contact:ContactInfo)=>{ListItem(){Row(){this.contactAvatar(contact.name)Column(){Text(contact.name).fontSize(16).fontColor('#333333').fontWeight(FontWeight.Medium)Text(contact.phone).fontSize(13).fontColor('#999999').margin({top:3})}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({left:12})Image($r('app.media.startIcon')).width(24).height(24).objectFit(ImageFit.Contain).opacity(0.4)}.width('100%').height(64).padding({left:16,right:16}).alignItems(VerticalAlign.Center).onClick(()=>{this.onContactClick(contact);})}.border({width:{bottom:0.5},color:{bottom:'#E8E8E8'}})},(contact:ContactInfo)=>contact.phone)}.divider({strokeWidth:0,startMargin:0,endMargin:0})},(groupItem:GroupData)=>groupItem.title)}.sticky(StickyStyle.Header).width('100%').height('100%').backgroundColor('#FFFFFF').edgeEffect(EdgeEffect.Spring)}.width('100%').height('100%').backgroundColor('#F7F7F7')}@BuildergroupHeaderBuilder(title:string){Text(title).fontSize(15).fontColor('#666666').fontWeight(FontWeight.Bold).width('100%').height(36).padding({left:16}).backgroundColor('#F0F0F0').textAlign(TextAlign.Start)}}十、总结
本文围绕 HarmonyOS NEXT 的List 分组(ListItemGroup)布局方式,以一个完整的通讯录风格应用为例,介绍了:
- 数据模型设计:用
ContactInfo和GroupData两层接口组织分组数据; - 组件层级结构:
List → ListItemGroup → ListItem的三层树形关系; - 核心属性用法:
header(分组标题)、sticky(吸顶)、divider(分组分隔线); - 交互与反馈:
onClick事件与showToast的组合使用; - API 注意事项:
BorderOptions在 API 24 中的类型变化; - 性能优化:
LazyForEach用于大数据量分组列表。
掌握List+ListItemGroup组合,就掌握了 HarmonyOS NEXT 应用中 80% 以上的列表页场景。从通讯录到商品分类,从好友列表到信息流,这种布局模式将是 ArkTS 开发工具箱中使用频率最高的利器。
本文代码基于 HarmonyOS NEXT API 24 编译验证。示例工程完整源码可在项目entry/src/main/ets/pages/ContactGroupList.ets路径下找到。