WebToApp 应用列表搜索功能全解析:My Apps 内置搜索的实现原理与使用指南
导读
本篇技术指南围绕 WebToApp(一款完全运行在手机上的 Android 端 Web 转 App 工具包)主界面My Apps(我的应用)内置的搜索功能展开,讲解如何通过顶栏放大镜按钮唤起内联搜索框、实时按应用名称或 URL 过滤应用列表,并深入 Compose UI 与 ViewModel 源码,揭示"边输入边过滤"背后的状态流与 300ms 防抖机制。读完本文,你既能熟练操作这一功能,也能从源码层面理解其数据流设计,为自定义或扩展类似搜索交互提供参考。
功能入口:My Apps 顶栏的放大镜按钮
搜索功能位于应用主界面。根据 My Apps 文档 对顶栏布局的描述,顶部栏从左到右依次为标题My Apps、深色/浅色切换、语言切换、搜索(🔍)与 更多(⋮)。
搜索按钮的实际交互由 HomeScreen.kt 实现。其中isSearchActive状态(第 156 行)控制搜索模式的开关,按钮图标在两种状态间切换:
- 未激活时显示放大镜图标(
Icons.Default.Search); - 激活后图标切换为关闭按钮(
Icons.Default.Close),点击即退出搜索并清空查询(第 301-308 行)。
// HomeScreen.kt 中搜索按钮的核心逻辑(示意) isSearchActive = !isSearchActive if (!isSearchActive) viewModel.search("") imageVector = if (isSearchActive) Icons.Default.Close else Icons.Default.Search这段代码说明:点击 🔍 将标题栏替换为搜索输入框;再次点击 ✕(关闭图标)则清除查询并关闭搜索,与 search.md 中的操作描述一一对应。
如何使用:三步完成应用过滤
根据 search.md 的说明,搜索交互非常轻量:
- 点击 🔍:标题栏被替换为一个内联搜索输入框,搜索状态被激活(
isSearchActive = true)。 - 输入关键字:应用列表随输入实时按应用名称(name)或 URL 过滤——列表随输入即时更新,无需回车确认。
- 点击 ✕ 清除:清空查询并关闭搜索,列表恢复显示当前分类下的全部应用。
从 UI 实现看,当isSearchActive为真时,WtaScreen的titleContent插槽被替换为一个WtaTextField输入框(HomeScreen.kt 第 211-220 行),其value绑定 ViewModel 的searchQuery状态,onValueChange直接调用viewModel.search(it)将每次按键写入状态流:
titleContent = if (isSearchActive) { { com.webtoapp.ui.design.WtaTextField( value = searchQuery, onValueChange = { viewModel.search(it) }, placeholder = Strings.search, singleLine = true, modifier = Modifier.fillMaxWidth() ) } } else nullviewModel.search(query)的实现非常简洁(MainViewModel.kt 第 504-506 行),仅将查询写入MutableStateFlow:
fun search(query: String) { _searchQuery.value = query }过滤逻辑源码解析:名称与 URL 的忽略大小写匹配
搜索的核心过滤逻辑在 MainViewModel.kt 的filteredApps与filteredSummaries两个 StateFlow 中。二者结构一致,分别服务于完整WebApp数据与列表摘要WebAppSummary数据。
以filteredSummaries为例(第 124-145 行),它通过combine合并三个数据源:
webAppSummaries—— 应用摘要列表;searchQuery.debounce(300)—— 搜索查询,带 300ms 防抖,避免每敲一个字符都触发重算;selectedCategoryId—— 当前选中的分类。
过滤分两步执行:
第一步:按分类过滤(第 131-135 行)
filtered = when (categoryId) { null -> filtered // 未选择分类:显示全部 -1L -> filtered.filter { it.categoryId == null } // 未分类应用 else -> filtered.filter { it.categoryId == categoryId } // 指定分类 }categoryId == null:显示全部应用;categoryId == -1L:仅显示未归类的应用;- 其他值:仅显示该分类下的应用。
第二步:按搜索词过滤(第 137-142 行)
if (query.isNotBlank()) { filtered = filtered.filter { it.name.contains(query, ignoreCase = true) || it.url.contains(query, ignoreCase = true) } }关键行为包括:
- 匹配字段:应用名称(
name)与应用 URL(url)两者任一命中即可; - 忽略大小写:
ignoreCase = true意味着输入github同样能匹配GitHub; - 子串匹配:使用
contains,只要关键字是名称或 URL 的子串即可命中,因此即使只记得网址片段也能快速定位应用; - 空查询跳过:
query.isNotBlank()保证未输入内容时不做无意义的过滤。
最终结果通过stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())对外暴露,UI 层以collectAsStateWithLifecycle()订阅(HomeScreen.kt 第 141-143 行),实现列表随输入实时刷新。
设计要点:搜索只过滤、不修改,且受分类约束
从上述源码可以归纳出该功能的三个设计要点:
- 纯过滤语义,零副作用:搜索只对当前可见列表做内存中的过滤,不会修改、删除或重建任何应用,也不会改动数据库(
WebApp数据本身在filteredApps/filteredSummaries中被原样保留,只是被筛选)。这与 search.md 中"Search only filters the visible list; it doesn't modify your apps"的说明完全一致。 - 在所选分类内搜索:过滤的第一步是分类过滤,第二步才是搜索词过滤,因此搜索结果始终限定在当前选中的分类内。未选中任何分类时才跨全部应用搜索。
- 响应式数据流:搜索词、分类选择、应用列表三者通过
combine响应式合并,任意一方变化都会触发重新过滤,配合 300ms 防抖在流畅度与实时性之间取得平衡。
搜索与其他主界面功能的配合可参考:分类管理(分类仅用于组织整理,不影响应用的构建与运行)、应用列表(每个应用一张卡片)与 创建应用(通过 Create 按钮进入应用类型选择器)。
总结
WebToApp 的 My Apps 搜索功能是一个典型的"轻交互、重数据流"设计:UI 层仅需一个布尔状态切换输入框显隐,所有过滤逻辑收敛在 ViewModel 的combine+debounce状态流中,以忽略大小写的子串匹配同时覆盖应用名称与 URL 两个字段,并且天然兼容分类过滤。对于需要在 Compose 中实现实时搜索列表的开发者而言,MainViewModel.kt 第 101-145 行的实现是一个可直接借鉴的范式:把搜索词提升为独立StateFlow、用debounce控制计算频率、以combine组合多源数据,即可获得清晰、可测试且性能友好的过滤链路。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考