导航与搜索:实现搜索功能、搜索结果的导航、使用Navigation处理搜索建议
搜索功能,几乎是每个App的标配。但搜索背后的导航逻辑,却常常被忽视。你想想看,用户从搜索框输入关键词,看到建议列表,点击某条建议跳转到详情页,再按返回键——这一连串的页面流转,如果处理不好,就会出现「回退错乱」或者「页面栈爆炸」的问题。
我个人习惯用Navigation组件来统一管理搜索流程。它天然支持深层链接、返回栈管理,配合SearchView或者自定义搜索框,能做出非常流畅的体验。今天我们就来聊聊,怎么用Navigation把搜索功能做得既专业又优雅。
搜索功能的核心流程
先理清思路。一个完整的搜索流程,通常包含三个环节:
- 输入与建议:用户输入关键词,实时展示搜索建议
- 结果展示:用户确认搜索,跳转到结果页面
- 结果导航:用户点击某条结果,进入详情页
这三个环节,每一步都涉及导航决策。我见过不少项目直接在Activity里用startActivity跳转,结果返回栈乱成一锅粥。用Navigation组件,我们可以把搜索流程定义成一张清晰的导航图。
核心原则:搜索页应该作为导航图的起点,搜索结果页和详情页作为它的子目标。这样返回时,用户能自然回到搜索页,而不是莫名其妙跳到首页。
定义导航图
假设我们有一个搜索页面SearchFragment,一个搜索结果页SearchResultFragment,一个详情页DetailFragment。导航图大概长这样:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/search_nav_graph"
app:startDestination="@id/searchFragment">
<fragment
android:id="@+id/searchFragment"
android:name=".ui.search.SearchFragment">
<action
android:id="@+id/action_search_to_searchResult"
app:destination="@id/searchResultFragment" />
</fragment>
<fragment
android:id="@+id/searchResultFragment"
android:name=".ui.search.SearchResultFragment">
<argument
android:name="query"
app:argType="string" />
<action
android:id="@+id/action_searchResult_to_detail"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name=".ui.detail.DetailFragment">
<argument
android:name="itemId"
app:argType="string" />
</fragment>
</navigation>
这里有个细节:SearchResultFragment接收一个query参数。为什么?因为搜索结果页需要知道用户搜了什么,才能展示对应的数据。而且,这个参数还能用来做深层链接——用户从外部点击一个搜索链接,可以直接打开搜索结果页。
实现搜索建议的导航
搜索建议,说白了就是用户还没输完,我们就猜他想搜什么。常见的做法是用一个RecyclerView展示建议列表。但点击建议后怎么导航?
我建议分两种情况处理:
- 建议是一个完整的搜索词:直接导航到SearchResultFragment,把建议词作为query参数传过去
- 建议是一个具体条目:比如用户搜「iPhone」,建议里出现「iPhone 15 Pro Max」,点击后直接跳转到DetailFragment
第二种情况更常见,也更容易踩坑。我曾经在一个电商项目里,用户点击搜索建议后直接跳到了详情页,但按返回键时,却回到了首页——因为详情页的返回栈里没有搜索页。后来我改用Navigation的深层链接来处理,才彻底解决。
具体做法是,在DetailFragment的导航定义里加上deepLink:
<deepLink app:uri="app://search/{itemId}" />
然后在点击建议时,构建一个PendingIntent或者直接调用NavController的navigate方法:
val deepLink = Uri.parse("app://search/${suggestion.itemId}")
findNavController().navigate(deepLink)
这样,用户从建议点击进入详情页,返回时依然能回到搜索页。返回栈是完整的。
小技巧:如果搜索建议来自网络,建议在本地缓存一份。用户点击建议时,即使网络延迟,也能立即响应。我习惯用Room做本地缓存,配合Paging3展示建议列表,体验非常丝滑。
搜索结果的导航与状态管理
搜索结果页本身也有导航逻辑。用户可能翻页、筛选、排序,这些操作都不应该丢失搜索上下文。我的做法是把搜索关键词和筛选条件都保存在ViewModel里,用SavedStateHandle来持久化。
class SearchResultViewModel(
savedStateHandle: SavedStateHandle,
private val repository: SearchRepository
) : ViewModel() {
val query: String = savedStateHandle.get<String>("query") ?: ""
val searchResults: StateFlow<List<SearchItem>> = repository
.search(query)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
这样,即使用户旋转屏幕或者被系统回收,搜索结果也不会丢失。而且,当用户从详情页返回时,搜索结果页的状态是完整的——不需要重新请求网络。
处理搜索建议的实时更新
搜索建议的实时更新,通常用debounce来避免频繁请求。我一般用Kotlin Flow的debounce操作符:
private val _query = MutableStateFlow("")
val suggestions: StateFlow<List<Suggestion>> = _query
.debounce(300)
.filter { it.length >= 2 }
.flatMapLatest { repository.getSuggestions(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun onQueryChanged(query: String) {
_query.value = query
}
这里有个坑:如果用户快速输入,debounce会取消上一次请求。但网络请求的取消需要配合协程的取消机制。我建议在repository层使用协程的取消支持,比如用Retrofit的suspend函数,或者用Flow的cancelable操作符。
注意:搜索建议的请求频率不要太高。即使有debounce,也要在服务端做限流。我曾经遇到过用户疯狂输入导致服务器被打挂的情况——嗯,从那以后我都在客户端和服务端双重限流。
用SVG梳理搜索导航流程
说了这么多,不如一张图来得直观。下面是我画的搜索导航流程图,展示了从输入到详情页的完整路径:
从图上可以清楚看到,搜索建议和搜索结果都指向详情页,但返回路径不同。建议点击走的是深层链接,返回时回到搜索页;结果点击走的是普通导航,返回时回到搜索结果页。这两种路径在Navigation组件里都能优雅地处理。
总结一下
搜索功能的导航,说白了就是三件事:定义好导航图、处理好建议的深层链接、管理好搜索结果的状态。用Navigation组件来做,代码清晰,逻辑严谨,不容易出bug。
我个人建议,在项目初期就把搜索导航图设计好,不要等到后期再补。因为搜索涉及多个页面的跳转和返回,一旦写死,改起来很痛苦。我曾经在一个项目里重构搜索导航,改了三天才把所有边界情况覆盖完——嗯,从那以后我都是先画图再写代码。
好了,今天的分享就到这里。如果你在实际项目中遇到搜索导航的问题,欢迎随时交流。