鸿蒙 ArkTS 实战:搜索列表 城市实时过滤(示例 94)

引言
搜索功能是现代应用中最高频的交互之一。从电商平台的商品搜索到通讯录的联系人查找,从音乐 App 的歌曲检索到城市列表的地区筛选,搜索框 + 列表的组合几乎出现在每一个需要数据检索的场景中。一个优秀的搜索体验需要满足三个核心要求:实时响应(用户输入即出结果)、模糊匹配(不需要精确输入也能找到)、清晰反馈(有结果时显示列表,无结果时给出提示)。
示例 94 以「城市搜索」为业务场景,实现了一个包含 20 个城市的可搜索列表。用户在搜索框中输入关键字,列表实时过滤显示匹配的城市;支持中文包含匹配(如输入「北」可匹配「北京」);搜索框旁边有「清空」按钮一键清除关键字;列表上方显示匹配结果数量;当没有匹配结果时,显示「未找到匹配城市」的空状态提示。整个流程涉及 TextInput 的 onChange 实时监听、get 计算属性的动态过滤、String.includes 的包含匹配、String.toLowerCase 的大小写不敏感处理、List + ForEach 的列表渲染、以及 if/else 条件渲染的空状态,几乎涵盖了搜索列表功能的全部核心要点。
这篇文章会严格按源码顺序,先介绍应用的整体功能与布局结构,再拆解实时搜索的核心逻辑,接着逐段解读 .ets 源码中的数据定义、过滤算法、列表渲染、空状态处理,然后分析 get 计算属性的特性、大小写不敏感匹配的技巧、条件渲染的策略,最后给出运行操作指南、可扩展方向与常见问题调试技巧。读完后,你不仅能看懂这一个城市搜索页面,还能举一反三,把它应用到联系人搜索、商品检索、文章查找等任何需要实时过滤的场景。
1. 应用概述与功能
「城市搜索」是一个面向数据检索场景的工具型页面,交互路径清晰:输入关键字 → 实时过滤列表 → 点击查看详情 → 可清空重置。
页面自上而下分为五块区域:顶部返回栏(返回按钮 + 标题「城市搜索」);搜索栏(TextInput 输入框 + 「清空」按钮);结果统计文本(「共 N 条结果」);结果列表(List + ForEach 渲染匹配的城市)或空状态提示(无匹配时显示);底部提示文字(「支持中文包含匹配,如输入"北"可匹配"北京"」)。
1.1 核心功能清单
- 实时搜索:用户在搜索框中输入关键字时,列表实时过滤,无需点击搜索按钮。
- 中文包含匹配:支持中文关键字的部分匹配,如输入「广」可匹配「广州」。
- 大小写不敏感:英文输入时自动转小写比较,如输入「bei」可匹配「Beijing」(如果数据中有英文)。
- 结果计数:搜索框下方实时显示匹配结果数量。
- 清空功能:点击「清空」按钮一键清除搜索关键字,恢复完整列表。
- 空状态提示:当没有匹配结果时,显示「未找到匹配城市」的友好提示,避免空白页面。
- 点击查看:点击列表项弹出 Toast 提示,模拟查看详情的交互。
1.2 技术要点一览
整个示例用到的关键技术对「实时搜索列表」类页面很有代表性:TextInput 的 onChange 事件实时监听输入、get 计算属性实现动态过滤(每次访问时重新计算)、String.trim() + String.toLowerCase() 的输入预处理、String.includes() 的包含匹配、List + ForEach 的列表渲染、if/else 条件分支处理有结果/无结果两种状态、以及 Blank() 弹性布局。把这些要点串起来,就构成了一条完整的「输入监听 → 关键字预处理 → 数组过滤 → 条件渲染」的搜索数据流。
2. 核心知识点
在逐段读代码之前,先把搜索列表页面承载的 ArkTS 核心知识讲清楚。
2.1 get 计算属性与实时过滤
ArkTS 中的 get 访问器可以定义计算属性——每次访问该属性时都会重新计算并返回结果:
get filterList(): string[] {
const k: string = this.keyword.trim().toLowerCase();
if (k.length === 0) {
return this.cities;
}
const result: string[] = [];
for (let i = 0; i < this.cities.length; i++) {
if (this.cities[i].toLowerCase().includes(k)) {
result.push(this.cities[i]);
}
}
return result;
}
filterList 是一个 get 计算属性,它不存储数据,而是在每次被访问时根据当前 this.keyword 的值动态计算过滤结果。当 keyword 为空时返回完整列表;不为空时遍历所有城市,将包含关键字的城市加入结果数组。
计算属性的优势在于声明式——你只需要定义「结果是什么」,不需要关心「何时重新计算」。ArkUI 的响应式系统会在 keyword 变化时自动重新调用 filterList 的 getter,获取新的过滤结果并更新 UI。
2.2 String.includes 包含匹配
String.prototype.includes 方法判断一个字符串是否包含另一个字符串:
this.cities[i].toLowerCase().includes(k)
includes 返回布尔值——true 表示包含,false 表示不包含。与 indexOf 不同,includes 直接返回布尔值,语义更清晰。例如:
'北京'.includes('北')→true'上海'.includes('北')→false'广州'.includes('广')→true
2.3 trim + toLowerCase 输入预处理
搜索关键字在匹配前经过两步预处理:
const k: string = this.keyword.trim().toLowerCase();
trim():去掉关键字首尾的空白字符。避免用户不小心输入了空格导致匹配失败。toLowerCase():将关键字转为小写。同时城市名称也调用toLowerCase(),实现大小写不敏感匹配。
这种预处理确保了搜索的容错性——用户输入「 北京 」(带空格)或「BEIJING」(大写),都能正确匹配到「北京」。
2.4 TextInput 的 onChange 实时监听
TextInput 组件的 onChange 事件在用户每次输入或删除字符时触发:
TextInput({ placeholder: '输入城市名称搜索', text: this.keyword })
.onChange((v: string) => {
this.keyword = v;
})
onChange 回调的参数 v 是输入框的当前完整文本。每次触发时,将 v 赋值给 this.keyword,触发 @State 变量变化,进而触发 filterList 计算属性重新计算,最终更新列表 UI。这就是「实时搜索」的实现原理——输入即触发,无需额外的搜索按钮。
text: this.keyword 实现了双向绑定——当 keyword 被清空时(如点击「清空」按钮),输入框的文字也会同步清空。
2.5 if/else 条件渲染
搜索结果区域使用 if/else 条件渲染处理有结果和无结果两种状态:
if (this.filterList.length > 0) {
List() {
ForEach(this.filterList, (city: string, idx: number) => {
ListItem() { ... }
}, ...)
}
} else {
Column() {
Text('未找到匹配城市') ...
Text('请尝试输入其他关键字') ...
}
}
当 filterList.length > 0 时渲染城市列表,否则渲染空状态提示。if/else 条件渲染是 ArkUI 中处理多状态 UI 的标准方式——只有条件为 true 的分支会被渲染到界面上,另一个分支的组件不会创建。
3. 源码逐段解析
现在开始按源码顺序逐段解读 index94.ets,从导入声明到 build 方法,完整展示搜索列表的实现细节。
3.1 导入声明与组件声明
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Index94 {
导入 router 和 promptAction,声明页面入口组件 Index94。
3.2 城市数据与搜索状态
@State cities: string[] = [
'北京', '上海', '广州', '深圳', '杭州',
'南京', '成都', '重庆', '武汉', '西安',
'天津', '苏州', '青岛', '大连', '厦门',
'长沙', '郑州', '昆明', '沈阳', '哈尔滨'
];
@State keyword: string = '';
cities 是包含 20 个城市的字符串数组,覆盖了国内主要的一二线城市。keyword 是搜索关键字,初始为空串(显示完整列表)。
注意 cities 使用了 @State 修饰,虽然在本示例中城市列表不会被修改,但使用 @State 确保了在需要动态更新列表时(如从网络加载)UI 能正确刷新。
3.3 filterList 计算属性
get filterList(): string[] {
const k: string = this.keyword.trim().toLowerCase();
if (k.length === 0) {
return this.cities;
}
const result: string[] = [];
for (let i = 0; i < this.cities.length; i++) {
if (this.cities[i].toLowerCase().includes(k)) {
result.push(this.cities[i]);
}
}
return result;
}
filterList 是整个搜索功能的核心。它的执行逻辑:
- 预处理关键字:
this.keyword.trim().toLowerCase()去除首尾空格并转小写。 - 空关键字短路:如果处理后的关键字长度为 0(即输入框为空或只有空格),直接返回完整城市列表,无需遍历。
- 遍历过滤:遍历所有城市,将每个城市名转小写后调用
includes(k)检查是否包含关键字。如果包含,加入结果数组。 - 返回结果:返回过滤后的城市数组。
注意这里同时处理了城市名和关键字的大小写转换——this.cities[i].toLowerCase() 和 k 都是小写,确保匹配不受大小写影响。对于中文,toLowerCase() 不会有任何影响(中文没有大小写之分),所以这个处理对中文搜索完全透明。
3.4 clearSearch 清空方法
private clearSearch(): void {
this.keyword = '';
}
clearSearch 方法非常简洁——只需将 keyword 设为空串。由于 keyword 是 @State 变量,赋空串后会触发:输入框文字清空(text: this.keyword 双向绑定)、filterList 重新计算返回完整列表、结果计数更新为 20、列表恢复完整显示。
3.5 build 方法整体结构
build() {
Column() {
// 顶部返回栏
Row() { ... }
// 搜索栏
Row() { ... }
// 结果统计
Text('共 ' + this.filterList.length + ' 条结果') ...
// 结果列表或空状态
if (this.filterList.length > 0) {
List() { ... }
} else {
Column() { ... }
}
// 底部提示
Text('支持中文包含匹配...') ...
}
.width('100%')
.height('100%')
.backgroundColor('#f2f3f5')
}
3.6 搜索栏
Row() {
TextInput({ placeholder: '输入城市名称搜索', text: this.keyword })
.layoutWeight(1)
.height(42)
.backgroundColor('#ffffff')
.borderRadius(8)
.onChange((v: string) => {
this.keyword = v;
})
Button('清空')
.height(42)
.backgroundColor('#1a6cff')
.fontColor(Color.White)
.margin({ left: 10 })
.onClick(() => {
this.clearSearch();
})
}
.width('100%')
.padding({ left: 12, right: 12 })
搜索栏使用 Row 布局,左侧是 TextInput 输入框,右侧是「清空」按钮:
TextInput使用layoutWeight(1)占据剩余空间,高度 42vp,白色背景圆角 8。text: this.keyword实现双向绑定。Button固定宽度,蓝色背景,左侧外边距 10vp 与输入框保持间距。
layoutWeight(1) 在这里很关键——它让 TextInput 自适应宽度,无论屏幕多宽都能填满「清空」按钮之外的所有空间。
3.7 结果统计
Text('共 ' + this.filterList.length + ' 条结果')
.fontSize(13)
.fontColor('#888888')
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 6 })
结果统计文本显示当前过滤后的城市数量。由于 filterList 是计算属性,每次 keyword 变化时都会重新计算,统计文本也会同步更新。例如输入「北」时显示「共 1 条结果」,输入「州」时显示「共 3 条结果」(广州、苏州、郑州)。
3.8 结果列表
if (this.filterList.length > 0) {
List() {
ForEach(this.filterList, (city: string, idx: number) => {
ListItem() {
Row() {
Text(city)
.fontSize(16)
.fontColor('#333333')
Blank()
Text('点击查看')
.fontSize(12)
.fontColor('#aaaaaa')
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.onClick(() => {
promptAction.showToast({ message: '点击了' + city });
})
}
}, (city: string, idx: number) => city + idx.toString())
}
.width('100%')
.layoutWeight(1)
}
结果列表使用 List + ForEach 渲染过滤后的城市。每个 ListItem 内部是一个 Row:
- 左侧显示城市名(16号深灰色字)。
- 右侧显示「点击查看」提示文字(12号浅灰色字)。
Blank()弹性分隔将两个文本推到两端。- 点击列表项弹出 Toast 提示。
ForEach 的键值生成器使用 city + idx.toString(),确保每个城市项有唯一标识,避免 ArkUI diff 算法出现重用错误。
List 设置 layoutWeight(1) 占据剩余空间,当城市较多时可以滚动浏览。
3.9 空状态提示
else {
Column() {
Text('未找到匹配城市')
.fontSize(15)
.fontColor('#aaaaaa')
Text('请尝试输入其他关键字')
.fontSize(12)
.fontColor('#cccccc')
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
当 filterList.length === 0 时(即搜索关键字没有匹配到任何城市),渲染空状态提示。Column 居中显示两行文字:
- 「未找到匹配城市」——15号浅灰色字,主提示。
- 「请尝试输入其他关键字」——12号更浅灰色字,辅助提示,上方间距 8vp。
justifyContent(FlexAlign.Center) 让内容垂直居中,避免提示文字出现在页面顶部或底部,影响视觉平衡。
3.10 底部提示
Text('支持中文包含匹配,如输入"北"可匹配"北京"')
.fontSize(12)
.fontColor('#aaaaaa')
.margin({ top: 10, bottom: 14 })
底部提示文字向用户说明搜索的匹配规则,降低使用门槛。
4. 交互流程详解
4.1 实时搜索流程
用户在搜索框中输入「广」:
TextInput的onChange回调触发,参数v为"广"。- 回调执行
this.keyword = "广",@State变量更新。 - ArkUI 响应式系统检测到
keyword变化,标记依赖keyword的 UI 需要重新渲染。 filterList计算属性被重新调用:"广".trim().toLowerCase()="广",遍历城市列表,"广州".includes("广")=true,"广元".includes("广")=true(如果有的话),返回["广州"]。- 结果统计文本更新为「共 1 条结果」。
List中的ForEach重新渲染,只显示「广州」一项。
4.2 清空搜索流程
用户点击「清空」按钮:
clearSearch()方法执行this.keyword = ''。TextInput的text绑定更新,输入框文字清空。filterList重新计算:"".trim().length === 0,返回完整城市列表。- 结果统计更新为「共 20 条结果」。
- 列表恢复显示所有 20 个城市。
4.3 空结果流程
用户输入「xyz」(不匹配任何城市):
this.keyword = "xyz"。filterList计算:所有城市的toLowerCase().includes("xyz")都为false,返回空数组[]。- 结果统计更新为「共 0 条结果」。
if (this.filterList.length > 0)条件为false,渲染else分支的空状态提示。- 列表区域显示「未找到匹配城市」居中提示。
4.4 点击列表项流程
用户点击某个城市:
ListItem的onClick回调触发。promptAction.showToast({ message: '点击了' + city })弹出 Toast。- Toast 持续约 2 秒后自动消失。
5. UI 样式设计思路
5.1 搜索栏布局
搜索栏采用 TextInput + Button 的水平排列,layoutWeight(1) 让输入框自适应宽度。这种布局在各种应用的搜索页面中非常常见,用户可以快速输入关键字并一键清空。
5.2 列表项布局
每个列表项使用 Row 布局,城市名在左、「点击查看」在右,通过 Blank() 弹性分隔。这种两端对齐的布局让列表项看起来整洁有序,同时右侧的提示文字引导用户进行下一步操作。
5.3 空状态设计
空状态提示使用居中布局,两行文字大小不同(15号和12号),颜色深浅不同,形成主次关系。这种设计比简单的「无结果」文字更友好,给出了具体的操作建议(「请尝试输入其他关键字」),帮助用户修正搜索。
5.4 结果计数
结果计数位于搜索栏和列表之间,用浅灰色小字显示。这个细节看似不起眼,但极大提升了搜索体验——用户可以立即知道搜索返回了多少条结果,决定是否需要修改关键字。
6. 运行与测试
6.1 运行步骤
- 使用 DevEco Studio 打开项目。
- 运行项目到模拟器或真机。
- 在首页找到「城市搜索」示例入口,点击进入。
- 在搜索框中输入不同关键字,观察列表实时过滤。
- 点击「清空」按钮,确认列表恢复完整。
- 输入不匹配的关键字,观察空状态提示。
6.2 测试场景
| 测试场景 | 预期结果 |
|---|---|
| 输入「北」 | 显示「北京」,共 1 条结果 |
| 输入「州」 | 显示「广州」「苏州」「郑州」,共 3 条结果 |
| 输入「海」 | 显示「上海」,共 1 条结果 |
| 输入「xyz」 | 显示「未找到匹配城市」空状态 |
| 点击「清空」按钮 | 输入框清空,列表恢复 20 个城市 |
| 输入空格「 」 | trim 后为空,显示完整列表(20 条) |
| 点击列表项「北京」 | 弹出 Toast「点击了北京」 |
7. 可扩展方向
7.1 拼音搜索
当前只支持中文字符匹配。可以集成拼音转换库,实现拼音搜索——输入「beijing」或「bj」也能匹配到「北京」。需要将城市名转为拼音后建立索引。
7.2 搜索高亮
在搜索结果中高亮显示匹配的关键字。例如输入「广」,列表中「广州」的「广」字用红色或加粗显示。需要在 Text 组件中使用 RichText 或分段 Text 实现部分文字高亮。
7.3 搜索历史
记录用户最近搜索的关键字,在搜索框为空时显示历史搜索列表。点击历史项可以直接搜索。需要使用 Preferences 持久化存储搜索历史。
7.4 分组显示
将搜索结果按首字母分组显示,如 A 组(安庆)、B 组(北京、包头)等。需要在数据中添加拼音首字母字段,并使用 ForEach 嵌套渲染分组标题和组内城市。
7.5 网络搜索
将城市数据改为从网络接口获取,支持更大数据量的搜索。需要添加网络请求逻辑和加载状态提示。
8. 常见问题与调试
8.1 搜索不实时
问题:输入文字后列表没有立即更新,需要点击其他地方才更新。
排查:
- 确认
TextInput的onChange回调中正确更新了this.keyword。 - 检查
filterList是否使用了get计算属性,而非普通方法。如果用普通方法,需要手动触发刷新。
8.2 大小写敏感
问题:输入大写字母无法匹配小写数据。
排查:
- 确认
filterList中对关键字和数据都调用了toLowerCase()。 - 检查是否有其他地方覆盖了
keyword的值导致预处理失效。
8.3 空格导致无结果
问题:输入「 北京」(带空格)没有匹配结果。
排查:
- 确认
filterList中对关键字调用了trim()去除首尾空格。 - 如果需要在数据中间匹配空格,不要对数据调用
trim()。
8.4 列表不滚动
问题:搜索结果较多时列表无法滚动。
排查:
- 确认
List设置了layoutWeight(1),让它占据剩余空间。如果List没有固定高度或layoutWeight,内容超出时不会滚动。 - 检查父容器
Column的高度是否为100%。
8.5 ForEach key 冲突
问题:列表渲染出现重复项或项错乱。
排查:
- 检查
ForEach的键值生成器。示例使用city + idx.toString(),如果城市名重复且索引相同,key 会冲突。 - 如果数据中有重复项,使用更复杂的 key 策略,如
city + '_' + idx.toString()。
9. 技术总结
示例 94 的搜索列表展示了实时搜索功能的完整实现。通过这个示例,我们可以总结出搜索列表的几个关键范式:
- get 计算属性:用
get定义过滤逻辑,声明式地表达「结果是什么」,ArkUI 自动处理何时重新计算。 - trim + toLowerCase:对关键字和数据都做预处理,确保搜索的容错性和大小写不敏感。
- includes 包含匹配:
String.includes是最简洁的包含判断方法,直接返回布尔值。 - if/else 条件渲染:用条件分支处理有结果和无结果两种 UI 状态,避免空白页面。
- layoutWeight 自适应:
TextInput使用layoutWeight(1)自适应宽度,List使用layoutWeight(1)占满剩余空间实现滚动。
掌握了这些范式后,就可以轻松地将实时搜索功能应用到联系人、商品、文章等各种数据检索场景中。搜索作为应用中最核心的交互之一,是每个鸿蒙开发者必须熟练掌握的功能模式。
更多推荐




所有评论(0)