不少做客户端开发的朋友问过我:Flutter 跑在 OpenHarmony 上,到底能不能做出体验接近原生的应用,尤其是搜索这种交互复杂、状态多、还牵扯到输入法和列表渲染的功能。这期实战系列就专门聊搜索模块的实现,从数据获取、状态管理到界面交互,把我在真机上跑通的关键路径和踩过的坑都摊开讲。如果你正在做 Flutter for OpenHarmony 的音乐播放器,或者打算评估这个技术栈的实际落地效果,这篇内容值得参考。
1. 整体设计与搜索场景拆解
1.1 搜索模块在音乐播放器里的定位
搜索功能不是简单的一个输入框加一个列表。在音乐播放器里,搜索通常是用户主动找歌、找专辑、找歌手的主要入口,使用频率高,且对响应速度、匹配准确度、状态可恢复性都有要求。更重要的是,搜索模块的代码会涉及到网络请求、本地历史记录、防抖、缓存、分页、拼音匹配等多个技术点,是一块很适合用来检验 Flutter 跨端能力的“试金石”。
我在设计的时候,把搜索模块拆成了三个核心职责:
- 搜索入口:负责接收用户输入,处理输入法组合态、防抖和提交逻辑。
- 数据服务:负责调用接口、拉取结果、记录历史,并把数据状态暴露给 UI 层。
- 结果展示:负责列表渲染、空态、错误态、加载态以及点击行为后的跳转逻辑。
为什么要把职责拆这么细?因为 OpenHarmony 上 Flutter 生态还不像 Android/iOS 那么成熟,社区里很多组件在鸿蒙上适配都可能出问题,如果搜索逻辑全部写在一个页面里,出问题很难定位。拆分成独立的服务层,至少你能针对数据层单独做 DCL(Design by Contract)测试,不用每次都启动整个 App 到真机上去点。
1.2 为什么选择自研而非依赖第三方搜索插件
在做搜索实现之前,我也评估过要不要直接引入 flutter_search、flutter_typeahead 这类成熟插件。但考量之后还是决定自研。原因有三个:
第一,OpenHarmony 上 Flutter 插件的兼容性是个不确定性因素。很多第三方插件依赖的 Android 原生代码在鸿蒙上无法直接使用,而鸿蒙系统的插件机制走的是 OpenHarmony 侧的 Plugin 桥接,并非所有 Flutter 插件都做了适配。搜索框这种组件看着简单,但下面挂着一堆平台通道的调用,一旦插件没适配鸿蒙,你连问题出在哪一层都很难排查。
第二,音乐播放器这种场景,搜索不只是“输入即时反馈”,背后还要关联到播放队列、收藏状态、历史记录去重、热搜词排序等一堆业务逻辑,这些东西第三方的搜索组件帮不上什么忙,反而是自己封装更能贴合业务。
第三,也是我个人的经验:搜索模块是后续功能扩展的高发地带。比如加入搜索排行榜、模糊搜索、拼音搜索,或者做搜索历史云同步。如果上来就依赖别人的组件,扩展点受限于别人预设的方案,到时候要么硬 hack,要么推倒重写。自研虽然开头多写了几百行代码,但后面省的事更多。
1.3 架构图思路与状态流转设计
搜索模块的整体链路我是这样设计的:
用户输入触发防抖计时器,计时器到期后发起搜索请求,请求返回后更新结果列表缓存,同时写入历史记录。如果用户直接点击搜索按钮,则跳过防抖,立即执行搜索;如果输入框清空,则重置到“默认状态”(这时候展示历史记录和热门搜索)。整个状态流转就是 输入中 -> 搜索中 -> 成功(有结果/无结果) / 失败,外加一个“历史记录模式”作为初始状态。
这里有一个容易被忽视的点:搜索模块的状态管理最好不要直接塞进页面 Widget 里。我建议单独建一个 SearchProvider,继承 ChangeNotifier,在里面维护 SearchState 枚举和结果列表。这样搜索页退出再进入的时候,状态还能保留(或者通过配置决定是否保留),不会被系统回收。
dart复制enum SearchState {
idle, // 初始状态,展示历史与热门
loading, // 搜索请求中
success, // 成功并返回结果
empty, // 成功但无数据
failure, // 网络或服务器异常
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据获取与搜索服务封装
2.1 数据模型设计
在实现搜索之前,先把数据模型定清楚。音乐播放器里的一次搜索结果,可能包含歌曲、专辑、歌手三个维度。实际开发中接口通常会返回一个混合列表,里面通过 type 字段区分实体类型。我在模型层建了一个 SearchResultItem,字段设计如下:
dart复制class SearchResultItem {
final String id;
final String name;
final String subName; // 歌曲的话是歌手名,歌手的话是别名/简介
final String coverUrl;
final String type; // song / album / artist / playlist
fi
