导航与搜索:实现搜索功能、搜索结果的导航、使用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梳理搜索导航流程

说了这么多,不如一张图来得直观。下面是我画的搜索导航流程图,展示了从输入到详情页的完整路径:

搜索导航流程图 搜索输入框 搜索建议列表 (实时更新,debounce 300ms) 点击建议 → 跳转详情 deepLink 详情页 返回 → 搜索页 回车/点击搜索 搜索结果页 展示搜索结果列表 支持翻页、筛选、排序 点击条目 返回键 → 回到搜索结果页 正常导航 深层链接导航 返回导航

从图上可以清楚看到,搜索建议和搜索结果都指向详情页,但返回路径不同。建议点击走的是深层链接,返回时回到搜索页;结果点击走的是普通导航,返回时回到搜索结果页。这两种路径在Navigation组件里都能优雅地处理。

总结一下

搜索功能的导航,说白了就是三件事:定义好导航图、处理好建议的深层链接、管理好搜索结果的状态。用Navigation组件来做,代码清晰,逻辑严谨,不容易出bug。

我个人建议,在项目初期就把搜索导航图设计好,不要等到后期再补。因为搜索涉及多个页面的跳转和返回,一旦写死,改起来很痛苦。我曾经在一个项目里重构搜索导航,改了三天才把所有边界情况覆盖完——嗯,从那以后我都是先画图再写代码。

好了,今天的分享就到这里。如果你在实际项目中遇到搜索导航的问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321