在这里插入图片描述

引言

搜索功能是现代应用中最高频的交互之一。从电商平台的商品搜索到通讯录的联系人查找,从音乐 App 的歌曲检索到城市列表的地区筛选,搜索框 + 列表的组合几乎出现在每一个需要数据检索的场景中。一个优秀的搜索体验需要满足三个核心要求:实时响应(用户输入即出结果)、模糊匹配(不需要精确输入也能找到)、清晰反馈(有结果时显示列表,无结果时给出提示)。

示例 94 以「城市搜索」为业务场景,实现了一个包含 20 个城市的可搜索列表。用户在搜索框中输入关键字,列表实时过滤显示匹配的城市;支持中文包含匹配(如输入「北」可匹配「北京」);搜索框旁边有「清空」按钮一键清除关键字;列表上方显示匹配结果数量;当没有匹配结果时,显示「未找到匹配城市」的空状态提示。整个流程涉及 TextInputonChange 实时监听、get 计算属性的动态过滤、String.includes 的包含匹配、String.toLowerCase 的大小写不敏感处理、List + ForEach 的列表渲染、以及 if/else 条件渲染的空状态,几乎涵盖了搜索列表功能的全部核心要点。

这篇文章会严格按源码顺序,先介绍应用的整体功能与布局结构,再拆解实时搜索的核心逻辑,接着逐段解读 .ets 源码中的数据定义、过滤算法、列表渲染、空状态处理,然后分析 get 计算属性的特性、大小写不敏感匹配的技巧、条件渲染的策略,最后给出运行操作指南、可扩展方向与常见问题调试技巧。读完后,你不仅能看懂这一个城市搜索页面,还能举一反三,把它应用到联系人搜索、商品检索、文章查找等任何需要实时过滤的场景。

1. 应用概述与功能

「城市搜索」是一个面向数据检索场景的工具型页面,交互路径清晰:输入关键字 → 实时过滤列表 → 点击查看详情 → 可清空重置

页面自上而下分为五块区域:顶部返回栏(返回按钮 + 标题「城市搜索」);搜索栏(TextInput 输入框 + 「清空」按钮);结果统计文本(「共 N 条结果」);结果列表(List + ForEach 渲染匹配的城市)或空状态提示(无匹配时显示);底部提示文字(「支持中文包含匹配,如输入"北"可匹配"北京"」)。

1.1 核心功能清单

  • 实时搜索:用户在搜索框中输入关键字时,列表实时过滤,无需点击搜索按钮。
  • 中文包含匹配:支持中文关键字的部分匹配,如输入「广」可匹配「广州」。
  • 大小写不敏感:英文输入时自动转小写比较,如输入「bei」可匹配「Beijing」(如果数据中有英文)。
  • 结果计数:搜索框下方实时显示匹配结果数量。
  • 清空功能:点击「清空」按钮一键清除搜索关键字,恢复完整列表。
  • 空状态提示:当没有匹配结果时,显示「未找到匹配城市」的友好提示,避免空白页面。
  • 点击查看:点击列表项弹出 Toast 提示,模拟查看详情的交互。

1.2 技术要点一览

整个示例用到的关键技术对「实时搜索列表」类页面很有代表性:TextInputonChange 事件实时监听输入、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();
  1. trim():去掉关键字首尾的空白字符。避免用户不小心输入了空格导致匹配失败。
  2. 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 {

导入 routerpromptAction,声明页面入口组件 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 是整个搜索功能的核心。它的执行逻辑:

  1. 预处理关键字this.keyword.trim().toLowerCase() 去除首尾空格并转小写。
  2. 空关键字短路:如果处理后的关键字长度为 0(即输入框为空或只有空格),直接返回完整城市列表,无需遍历。
  3. 遍历过滤:遍历所有城市,将每个城市名转小写后调用 includes(k) 检查是否包含关键字。如果包含,加入结果数组。
  4. 返回结果:返回过滤后的城市数组。

注意这里同时处理了城市名和关键字的大小写转换——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 实时搜索流程

用户在搜索框中输入「广」:

  1. TextInputonChange 回调触发,参数 v"广"
  2. 回调执行 this.keyword = "广"@State 变量更新。
  3. ArkUI 响应式系统检测到 keyword 变化,标记依赖 keyword 的 UI 需要重新渲染。
  4. filterList 计算属性被重新调用:"广".trim().toLowerCase() = "广",遍历城市列表,"广州".includes("广") = true"广元".includes("广") = true(如果有的话),返回 ["广州"]
  5. 结果统计文本更新为「共 1 条结果」。
  6. List 中的 ForEach 重新渲染,只显示「广州」一项。

4.2 清空搜索流程

用户点击「清空」按钮:

  1. clearSearch() 方法执行 this.keyword = ''
  2. TextInputtext 绑定更新,输入框文字清空。
  3. filterList 重新计算:"".trim().length === 0,返回完整城市列表。
  4. 结果统计更新为「共 20 条结果」。
  5. 列表恢复显示所有 20 个城市。

4.3 空结果流程

用户输入「xyz」(不匹配任何城市):

  1. this.keyword = "xyz"
  2. filterList 计算:所有城市的 toLowerCase().includes("xyz") 都为 false,返回空数组 []
  3. 结果统计更新为「共 0 条结果」。
  4. if (this.filterList.length > 0) 条件为 false,渲染 else 分支的空状态提示。
  5. 列表区域显示「未找到匹配城市」居中提示。

4.4 点击列表项流程

用户点击某个城市:

  1. ListItemonClick 回调触发。
  2. promptAction.showToast({ message: '点击了' + city }) 弹出 Toast。
  3. Toast 持续约 2 秒后自动消失。

5. UI 样式设计思路

5.1 搜索栏布局

搜索栏采用 TextInput + Button 的水平排列,layoutWeight(1) 让输入框自适应宽度。这种布局在各种应用的搜索页面中非常常见,用户可以快速输入关键字并一键清空。

5.2 列表项布局

每个列表项使用 Row 布局,城市名在左、「点击查看」在右,通过 Blank() 弹性分隔。这种两端对齐的布局让列表项看起来整洁有序,同时右侧的提示文字引导用户进行下一步操作。

5.3 空状态设计

空状态提示使用居中布局,两行文字大小不同(15号和12号),颜色深浅不同,形成主次关系。这种设计比简单的「无结果」文字更友好,给出了具体的操作建议(「请尝试输入其他关键字」),帮助用户修正搜索。

5.4 结果计数

结果计数位于搜索栏和列表之间,用浅灰色小字显示。这个细节看似不起眼,但极大提升了搜索体验——用户可以立即知道搜索返回了多少条结果,决定是否需要修改关键字。

6. 运行与测试

6.1 运行步骤

  1. 使用 DevEco Studio 打开项目。
  2. 运行项目到模拟器或真机。
  3. 在首页找到「城市搜索」示例入口,点击进入。
  4. 在搜索框中输入不同关键字,观察列表实时过滤。
  5. 点击「清空」按钮,确认列表恢复完整。
  6. 输入不匹配的关键字,观察空状态提示。

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 搜索不实时

问题:输入文字后列表没有立即更新,需要点击其他地方才更新。

排查

  • 确认 TextInputonChange 回调中正确更新了 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 的搜索列表展示了实时搜索功能的完整实现。通过这个示例,我们可以总结出搜索列表的几个关键范式:

  1. get 计算属性:用 get 定义过滤逻辑,声明式地表达「结果是什么」,ArkUI 自动处理何时重新计算。
  2. trim + toLowerCase:对关键字和数据都做预处理,确保搜索的容错性和大小写不敏感。
  3. includes 包含匹配String.includes 是最简洁的包含判断方法,直接返回布尔值。
  4. if/else 条件渲染:用条件分支处理有结果和无结果两种 UI 状态,避免空白页面。
  5. layoutWeight 自适应TextInput 使用 layoutWeight(1) 自适应宽度,List 使用 layoutWeight(1) 占满剩余空间实现滚动。

掌握了这些范式后,就可以轻松地将实时搜索功能应用到联系人、商品、文章等各种数据检索场景中。搜索作为应用中最核心的交互之一,是每个鸿蒙开发者必须熟练掌握的功能模式。

Logo

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

更多推荐