1. 搜索模块整体设计思路
做音乐播放器App,搜索功能绝对不是随便放个输入框加个列表那么简单。我最初搭Flutter for OpenHarmony这套项目框架时,就把搜索单独拆成了一个完整模块来规划,原因很简单:搜索背后牵扯到的交互状态、数据流、播放联动,比大多数页面都要复杂。
先说需求拆解。一个音乐播放器里的搜索,至少覆盖这几类场景:用户输入关键词搜单曲、搜歌手、搜专辑;搜索结果出来之后可以直接点击播放;搜索页还需要有搜索历史记录、热门搜索推荐,以及空状态、加载状态、错误状态的处理。如果产品的定位是聚合型音乐播放器,可能还要支持多数据源的搜索结果合并展示,比如同时返回本地音乐和在线音乐的结果。这个App的搜索模块,我按“输入交互-请求调度-结果展示-播放联动”四条线来切分,核心目标就一个:让用户从输入关键词到听到歌,整个链路尽量短、尽量顺。
技术选型上需要特别说明一点。虽然项目用的是Flutter,但目标平台是OpenHarmony,这跟在Android/iOS上写Flutter有一些本质差别。Flutter的UI层逻辑是跨端一致的,但涉及平台通道(Platform Channel)的调用、系统媒体服务(AVSession)的对接、联网权限的申请,都必须考虑OpenHarmony的实现方式。 搜索功能看着是纯UI交互,实际上它要跨平台拿搜索结果、要调本地数据库做历史记录、要联动底层播放器,所以在架构上我明确划分了纯Dart层和平台相关层的边界。Dart层负责状态管理、数据模型、请求编排,平台相关层只负责具体的媒体服务调用和系统能力接入。
搜索功能的界面结构我也提前画好了。顶部是搜索输入框,带取消按钮;下方是搜索历史展示区和热门搜索标签区;当用户提交关键词后,切换为搜索结果列表页,列表项展示歌曲名、歌手、专辑,右侧是播放和收藏入口。这个结构看起来常规,但真正实现时,输入框的焦点管理、软键盘弹起与列表滚动冲突、搜索防抖时机这些细节,每一个都会影响体验。
这套设计思路从代码组织上也有体现。我在lib目录下建了search_page.dart、search_model.dart、search_result_item.dart、search_history_manager.dart等多个文件,各司其职。做的过程中踩了不少坑,后面会详细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Flutter搜索交互层实现细节
2.1 搜索输入框的焦点与防抖处理
搜索页的输入体验是第一个需要抠细节的地方。很多新手写搜索框,直接用TextField就完事儿了,实际用起来就会发现一堆问题:点击搜索页进入时键盘不弹出来、输入太频繁导致请求风暴、清除按钮点击后焦点丢失、搜索结果滚动时软键盘不收起等等。
我在这个项目里用了FocusNode来手动管理焦点生命周期。页面initState时创建FocusNode,等待页面build完成后请求焦点,让软键盘自动弹出:
dart复制class _SearchPageState extends State<SearchPage> {
final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController();
Timer? _debounceTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_searchFocusNode.requestFocus();
});
}
这里有个细节:直接在建树前请求焦点是拿不到焦点的,因为TextField可能还没完成布局。用addPostFrameCallback等第一帧绘制完再请求,成功率最高。在OpenHarmony上的Flutter环境,这个时序问题比Android上更明显,因为OpenHarmony对Flutter的接入层会有额外的帧调度延迟。
防抖是搜索请求的关键。我设了400毫秒的防抖窗口,用户停止输入400毫秒后才真正触发搜索请求。实现靠一个Timer来取消上一次未触发的请求:
dart复制void _onSearchTextChanged(String value) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 400), () {
_performSearch(value.trim());
});
}
防抖时间不能太短也不能太长。太短,请求依然频繁;太长,用户会觉得搜索卡顿。我实测下来400毫秒比较均衡。注意在dispose时一定要取消Timer,否则页面销毁后回调还在执行,轻则报错,重则内存泄漏。
2.2 搜索建议与键盘动作的配合
搜索框另外一个体验细节是键盘右下角按钮的动作类型。我用textInputAction属性设为TextInputAction.search,这样键盘右下角显示的是“搜索”按钮,点击后直接触发搜索提交而不是换行。这个细节很多App都忽略了,导致用户在搜索框里换行,非常不专业。
顺便把搜索建议也做了。这里的搜索建议不是那种智能联想下拉列表,最简单的做法是把热门搜索词和历史搜索词放在搜索框下方,用户点击标签直接补全或提交。我维护了一个_searchHistory列表,用SharedPreferences做本地持久化,存取都很方便:
dart复制class SearchHistoryManager {
static const String _key = 'search_history';
static const int _maxCount = 10;
static Future<List<String>> load() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getStringList(_key) ?? [];
}
static Future<void> add(String keyword) async {
final prefs = await SharedPreferences.getInstance();
final list = prefs.ge
