HarmonyOS 7 AtomicServiceSearch:文搜图输入预上屏提交代次隔离【鸿蒙心迹】
同一个搜索框里,用户输入“秋天”,随后补成“秋天的银杏”,肉眼看来不过是多敲几个字。放到真实相册检索,问题并不在搜索按钮本身:输入法可能仍处于预上屏阶段,前一次检索可能尚未返回,用户也可能连续点击两次提交。要是把每次 onChange 都视为一次正式查询,再按回包顺序覆盖屏幕,结果区就会在两个不同查询之间跳来跳去。界面上看到的关键词是银杏,下面却短暂出现“秋天”的泛化结果,这种错位比一次慢查询更让人难以信任。
这一篇只讨论输入与结果之间的提交协议,不重复语义向量模型训练、照片Embedding生成、索引分桶或瀑布流缩略图回填。项目叫 QueryCommitLab,主页面 QueryPage,诊断页面 SubmitAuditPage,负责把草稿文字、正式提交和异步结果拆成三条不同的时间线。示例数据集 album_semantic_31,任务编号 QRY-1010-18。固定事件中有六次草稿变化、三次提交动作、两次有效提交、一次重复提交忽略、一次旧结果忽略,最后一版提交代次为3,最终展示七张“秋天的银杏”照片,业务状态 RESULT_READY。这些都是应用层演练数据,真实检索引擎在本文标记为 FIXTURE_ONLY。

一、从“文字已经显示”到“查询已经生效”之间还有一道边界
1. 输入框显示值不是请求凭证
移动端搜索框里有至少三种值得分开的状态。第一种是输入草稿,编辑、删除、输入法候选词改变都会影响它;第二种是用户正式确认的查询,通常由键盘搜索键或搜索按钮触发;第三种是已经取回并通过当前版本校验的结果。三种状态可能在不同毫秒内发生,不能用一个 query 字符串代表全部生命周期。尤其是输入法预上屏机制存在时,按键、候选上屏、组件事件与用户意图不必恰好一一对应。
华为文档中的 AtomicServiceSearch 提供搜索区 search 配置,含 onChange、onSubmit 和 enablePreviewText 等能力。这个事实可以作为界面事件的锚点,但它本身并不替业务决定哪次回包更新UI。我们的约束比较保守:onChange 只维护显示用草稿;onSubmit 才生成新的业务提交版本。预上屏发生时不自行猜测“候选词已定稿”,也不假定框架一定提供额外的合成开始或结束回调。关于输入法的细节,业务只认最后一次确认提交的字符串。
本轮示例将草稿变化统计为六次,它不等同于六次网络查询。用户可以连续修改草稿,而真正创建请求的次数只有两次。第三次 onSubmit 因与最新已提交文本相同,被业务去重。这里的“相同”在约定范围内只做首尾空白清理和连续空白折叠,不把繁简体、同音词或语义相近的句子擅自归为同一个检索任务。若产品确实需要语言归一或纠错,应把具体规则和版本放到独立的检索规范里。
2. 为什么不在每个 onChange 都发请求
按变化实时预取并非绝对错误,问题在于不能将预取与正式提交混为一谈。一旦多个输入事件都带来异步任务,任务取消、输入法预上屏、焦点变化和页面切换会引出更多生命周期分支;即使做了节流,较旧的请求也可能在较新的请求之后返回。节流解决请求频率,不能替代结果归属。为了先把工程边界讲清,本例把预取功能关闭,待提交协议稳定后再考虑把预取做成“只能暖缓存、不得直接渲染”的第二条通道。
本例的确定性事件顺序是:09:18:10提交“秋天”得到代次2;09:18:12提交“秋天的银杏”得到代次3;同一时刻重复提交银杏被忽略;09:18:13代次2的旧结果返回但不得写屏;09:18:14代次3返回七张图并应用。提交编号从1开始留给页面基线,因而两次有效提交后的 queryRevision 为3。代次与请求标识不是从时间戳截取出来的“看起来唯一”字符串,而是同一页面会话内递增的整数;页面销毁重建时需要同时更换会话身份,防止旧实例的回调碰巧与新实例撞号。
二、给业务提交加一个不依赖组件的闸门
先处理核心问题:如何让重复提交不产生新版本,以及如何保证旧回包不能覆盖新查询。把这部分写成普通ArkTS类,可单独注入固定输入,不必先启动DevEco设备。接口只接收已确认提交的文字,返回 undefined 表示重复或空文本,返回不可变快照表示新任务。accepted 是业务计数,不代表平台搜索成功。
interface QueryTicket {
sessionId: string;
revision: number;
text: string;
}
class QueryCommitGate {
private revision: number = 1;
private lastCommitted: string = '';
private sessionId: string = 'QRY-1010-18';
accepted: number = 0;
duplicateIgnored: number = 0;
staleResultsIgnored: number = 0;
private normalize(input: string): string {
return input.trim().replace(/\s+/g, ' ');
}
commit(raw: string): QueryTicket | undefined {
const normalized = this.normalize(raw);
if (normalized.length === 0) {
return undefined;
}
if (normalized === this.lastCommitted) {
this.duplicateIgnored++;
return undefined;
}
this.lastCommitted = normalized;
this.revision++;
this.accepted++;
return { sessionId: this.sessionId,
revision: this.revision, text: normalized };
}
mayApply(ticket: QueryTicket): boolean {
const ok = ticket.sessionId === this.sessionId &&
ticket.revision === this.revision;
if (!ok) this.staleResultsIgnored++;
return ok;
}
get currentRevision(): number { return this.revision; }
get latestText(): string { return this.lastCommitted; }
close(): void { this.sessionId = this.sessionId + '-closed'; }
}
这个类刻意不提供“等待某个定时器再决定是否提交”的内部逻辑。按键确认为哪次事件,属于页面组件与业务控制器的责任;闸门只对已经到来的提交做规则判断。它也没有假装能停止已经发给外部服务的请求,mayApply 的作用只是禁止旧结果进入当前状态。真实请求如果具备取消接口,当然可以额外释放后台成本,但取消不应当是唯一正确性保障:取消与回调可能同时发生,服务端也可能已经处理完任务。
close() 将当前会话作废,解决页面退出后的迟到回调。真正工程里建议使用随机会话ID或UIAbility生命周期生成的单调序列,示例拼接 -closed 只是便于展示可验证的失效规则。若状态需要跨进程恢复,还应该存储提交快照和业务数据版本,不能把页面临时内存当作持久化协议。重复提交的判断也可以拓展为“相同文本+相同筛选器+同一数据集修订”,否则用户主动调整排序却被误判为重复。
三、AtomicServiceSearch 的事件只负责事件,不替数据背书
页面侧需要解决两个具体动作:六次草稿变动要即时反映输入框,正式提交则交给闸门决定是否安排搜索。示例使用文档中公开的 AtomicServiceSearch 与 SearchParams 搜索区事件。为免把未确认模型SDK写成可直接运行的工程事实,异步返回由本地 FixtureSearchPort 生成;把它换成真正文搜图SDK前,必须核对该SDK的签名、权限、索引准备状态和回调线程。
import { AtomicServiceSearch } from '@kit.ArkUI';
interface PhotoHit { id: string; label: string; }
class FixtureSearchPort {
async search(ticket: QueryTicket): Promise<PhotoHit[]> {
// 固定夹具:这里只表达返回数据,不实际调用系统相册或AI模型。
if (ticket.text === '秋天的银杏') {
return ['P01','P02','P03','P04','P05','P06','P07']
.map((id: string) => ({ id, label: '银杏主题照片' }));
}
return [{ id: 'OLD01', label: '秋天泛查询' }];
}
}
@Entry
@Component
struct QueryPage {
@State private draft: string = '';
@State private resultCount: number = 0;
@State private stateText: string = 'READY';
private gate: QueryCommitGate = new QueryCommitGate();
private port: FixtureSearchPort = new FixtureSearchPort();
private async submitQuery(text: string): Promise<void> {
const ticket = this.gate.commit(text);
if (!ticket) return;
this.stateText = 'LOADING';
try {
const hits = await this.port.search(ticket);
if (!this.gate.mayApply(ticket)) return;
this.resultCount = hits.length;
this.stateText = 'RESULT_READY';
} catch (_err) {
if (this.gate.mayApply(ticket)) this.stateText = 'RETRYABLE';
}
}
aboutToDisappear(): void { this.gate.close(); }
build() {
Column() {
AtomicServiceSearch({
value: this.draft,
placeholder: '描述你想找的照片',
search: {
enablePreviewText: true,
onChange: (value: string) => { this.draft = value; },
onSubmit: (value: string) => { this.submitQuery(value); }
}
});
Text(`结果 ${this.resultCount} / ${this.stateText}`);
}
}
}
在框架层 onSubmit 不需要和按钮 onClick 争夺“唯一提交入口”。如果外部另有实体搜索按钮,让按钮调用同一个 submitQuery() 即可,不要复制另一套异步处理路径。输入框焦点失去后是否继续显示键盘,与查询版号无关;屏幕旋转、折叠展开或返回页面同理。如果主页面将来被导航栈缓存而不销毁,要区分“隐藏”与“释放”:隐藏时是否接受回包取决于产品选择,但不应绕过会话校验。若页面明确离开且不再接受结果,就作废该会话;返回时用新的会话重新建立视图。
注意示例代码中结果数量没有在 onChange 被直接清零。这是设计上的选择:输入草稿变化但尚未提交时,历史结果仍代表上次已提交文本,可以用UI标签“当前结果对应上次提交”明确提示。若产品希望即时清空,也应把“空状态”标注为未提交草稿,不能展示为新查询的空结果。当前演示图中只展示最后提交完成的结果态,以免将中间态误认为真机截图。

四、让结果图与诊断图承担不同证据
上图是白色DevEco Studio风格的拟真演示图:左目录、中编辑器、右模拟器、底部HiLog。它不是DevEco实际编译结果。调试时真正重要的是三个日志字段必须同时出现:提交版本、对应查询、是否允许写屏。只打印“搜索成功”没有归属证据;只打印回调时间又容易把后返回误认为更新的任务。本例在HiLog样例中保留 QRY-1010-18 submits=3 accepted=2 duplicate=1、revision=3 staleResult=1 results=7 RESULT_READY、engine=FIXTURE_ONLY,这三个结果与业务闸门的计数含义一致。
七张图片用的是固定夹具中的可视化示意卡片,不存在真实的语义相似度分数、云端推理耗时或相册扫描记录。这里没有理由为截图编造“向量召回率97%”之类无法验证的数字。真正接入检索服务后,结果集还应当带模型版本、索引快照号、分页游标、授权结果和可能的空图库条件。业务规则可以在同一个 QueryTicket 上扩展字段,但应该在提交时一次冻结;不能回包到了才读取当时最新的过滤器,否则前后两次查询仍可能混淆。

主运行示意页显示 QRY-1010-18、album_semantic_31、六次输入修改、三次提交、两次接纳、一次重复提交忽略、一次迟到结果忽略、提交代次3和七张结果。真正的价值不是这些样例数值本身,而是它们能构成一份互相核对的账本。比如如果提交事件计数是3,接受2、重复忽略1,且没有空文本事件,那么两边必须相加等于3;迟到结果计数单独来自结果层,不得拿来凑提交层的算术。
诊断页要保留时序,而不重复贴七张照片。第一条“秋天”在09:18:10被接纳为代次2;第二条银杏查询在09:18:12升级为代次3;第三次银杏提交没有新代次;09:18:13代次2回包只记 STALE;09:18:14代次3回包七条记 APPLY。当用户反馈“输入银杏却看到了旧照片”,我们先看这五条事件是否完整,再查具体结果集,而不是一上来怀疑索引模型语义能力。设计上只有在后端结果已正确归属后,相关度质量才值得被讨论。

五、用固定夹具先暴露状态协议的漏洞
1. 不能拿真实异步顺序碰运气
测试夹具应允许指定每条结果的返回顺序,让旧查询刻意晚于新查询完成。如果只在网络良好、单次键盘提交的情况下“手工点两下”,大部分竞态不会出现。第一组用例验证相同文本重复提交不增加版本;第二组验证旧结果即使最后返回也不能写UI;第三组验证空字符串、全空白和首尾空白处理;第四组验证页面离开后会话失效;第五组验证筛选器改变时必须生成新提交标识。每组都应当同时断言状态和计数,不能仅检查最终列表是否非空。
这里最容易写错的是“判定顺序”。例如 mayApply 失败时若先更新 resultCount 再返回,屏幕仍会短暂闪出错误数量。应将会话与代次校验放在任何业务状态写入之前;错误回调同样校验,因为上一轮搜索失败也不应该把当前已成功的新查询改成 RETRYABLE。如果未来还有分页追加或图片懒加载,分页游标也须绑定已提交版本,不能只比较用户界面中显示的文本。
再看页面关闭:组件释放并不等于异步回调对象一定不再可达。Promise保留闭包的时间取决于任务完成,故而页面若提前退出,回调仍可能抵达。close() 作废会话能够防止逻辑写入,但如果还持有实际PixelMap、文件句柄或网络连接,仍需要在业务层实现对应的显式取消与释放。前者维护状态正确性,后者控制资源成本,两者不要互相替代。
2. 搜索范围与隐私访问另外验收
文搜图意味着照片可能来自用户私有图库。本文没有读取真实资产,也没有假定授权一旦给出就永久有效。项目接入媒体库时,实际服务应当处理用户取消选择、权限被撤回、图片被删除、跨设备资源暂不可达等返回条件。搜索UI不可把数据集名称当成权限凭证;album_semantic_31 只是模拟素材集标识。真实产品还要明确索引是否在端侧、检索是否上传文本、上传前如何做敏感词与隐私声明,这些不属于本篇可声称已经验收的范围。
同样不能把 enablePreviewText 误解为业务已获得“输入法合成开始”“候选词最终确认”的完整协议。它描述组件对预上屏文本的支持,不保证每一种输入法都有相同的事件数量。最稳妥的写法就是让提交归属独立于按键次数;即使未来接入语音输入、OCR提取或快捷搜索建议,只要全部走同一业务提交闸门,旧结果也无法覆盖新结果。工程收益来自协议收口,而非某个输入框拥有神奇能力。
六、交付前要把哪些数据写进验收证据
应用日志至少应包括任务ID、会话ID、提交代次、提交时归一后的字符串摘要、对应的结果集版本及裁决原因;为了避免日志泄露私人搜索词,生产日志可以只存哈希、长度和合法化的业务标签,全文仅在开发演示夹具中使用。日志必须足以区分重复提交与过期结果:前者发生在 commit,后者发生在 mayApply,它们分别影响提交次数与回包次数。若两个计数混在一个 ignored 字段里,现场排障会失去定位价值。
页面侧还应当在空结果、报错重试和页面快速来回切换时给出明确交互反馈。空结果意味着当前有效提交返回零个匹配,不是输入草稿为空;错误重试要复用还是生成新版本,需有显式策略。当前方案建议重试沿用原 QueryTicket,前提是它仍为当前版本;用户一旦提交了新文本,旧重试立即失去写屏资格。这个决定看上去琐碎,却能避免“重试”按钮在不知不觉中把用户拉回上一段查询。
最终夹具应输出一份可重放的事件记录,包含六次草稿变化、三次提交、一次重复忽略、两次有效任务、一次旧结果丢弃和七条最新结果。这份记录的质量高于某张成功截图,因为它不仅描述最终状态,也解释最终状态从何而来。不过它也只验证了应用层函数,不等于HarmonyOS设备输入法兼容性或真正语义搜索准确率。后续真机验收应覆盖中英文混输、拼音候选、多窗口焦点切换、弱网回包乱序、权限撤销及性能基线;在这些测试完成之前,发布文章应始终把本例称作设计与固定输入演练。
还可以补上一项常被忽视的无障碍验收:输入时朗读焦点仍属于搜索框,结果更新后不应强制将焦点跳到第一张照片,更不能因为旧请求返回而触发无意义的列表重排提示。状态播报应绑定正式提交成功后的结果版本,并让用户自主进入结果列表。对键盘、触控笔和语音输入三种入口,最好分别记录可重现的提交序列;不同入口只改变草稿的形成方式,不改变最终版本归属。此外,在折叠屏外屏向内屏切换期间,搜索框实例可能重建,诊断时需把页面会话身份纳入日志,否则看到的“版本3”未必来自同一个界面实例。这样的验证关心的是信息可理解性,而不只是没有发生技术异常。
七、收束:不要让展示文字和数据责任缠在一起
这一轮没有试图实现一个更强的AI模型,甚至没有接入真正的语义检索SDK。它只是把一句看起来普通的“秋天的银杏”拆成可审计的提交快照与结果回写协议。AtomicServiceSearch负责接收输入事件,QueryCommitGate负责决定提交是否有效,结果服务负责返回对应版本,页面只在最终校验通过后更新列表。模块职责清楚之后,未来叠加索引、推荐、筛选或跨设备能力才有可靠的起点。
比较合理的下一步不是立刻追求更快,而是先在DevEco和真机上复现输入法预上屏、重复回车、返回页重建和弱网乱序。只有当日志能解释每一次没有写屏的原因,搜索页面才算具备工程上可维护的边界。本文七张图片、代次3和 RESULT_READY 都只是示意演练结果,不应被当成实际项目的性能或功能认证。
更多推荐



所有评论(0)