You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用MutableStateFlow实现Android列表即时搜索过滤功能

Kotlin Flow 实现本地列表即时搜索完整方案

一、ViewModel 层核心逻辑实现

先定义统一的UI状态类,承载列表数据、空结果状态:

// 数据类根据你的现有模型调整即可
data class ExploreUiState(
    val topicList: List<Topic> = emptyList(),
    val showEmptyTip: Boolean = false,
    val isLoading: Boolean = false
)

ViewModel 内通过两个基础StateFlow承载原始数据和搜索关键词,搭配Flow操作符实现过滤逻辑:

class ExploreViewModel : ViewModel() {
    // 全量主题数据源:服务端数据返回、flatMap合并所有分类下Topic后更新
    private val _allTopicsFlow = MutableStateFlow<List<Topic>>(emptyList())
    // 搜索关键词流:搜索框输入变化时更新
    private val _searchQueryFlow = MutableStateFlow("")

    // 暴露给UI层的最终状态
    val uiState: StateFlow<ExploreUiState> = combine(
        _allTopicsFlow,
        _searchQueryFlow
            .debounce(300) // 输入停顿300ms再触发过滤,避免频繁计算
            .distinctUntilChanged() // 关键词和上一次完全一致时不重复执行逻辑
    ) { allTopics, rawQuery ->
        val query = rawQuery.trim().lowercase(Locale.getDefault())
        val filteredList = if (query.isEmpty()) {
            allTopics
        } else {
            // 统一转小写实现大小写不敏感匹配
            allTopics.filter { 
                it.title.lowercase(Locale.getDefault()).contains(query) 
            }
        }
        ExploreUiState(
            topicList = filteredList,
            showEmptyTip = query.isNotEmpty() && filteredList.isEmpty()
        )
    }
        .flowOn(Dispatchers.Default) // 过滤逻辑切到子线程执行,避免主线程卡顿
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = ExploreUiState(isLoading = true)
        )

    // 搜索框输入回调调用
    fun updateSearchQuery(query: String) {
        _searchQueryFlow.value = query
    }

    // 服务端分类数据返回后调用,合并所有Topic更新全量数据源
    fun submitCategoryData(categories: List<Category>) {
        _allTopicsFlow.value = categories.flatMap { it.topics }
    }
}

二、页面层接入逻辑

搜索框监听

避免TextWatcher重复回调,只在输入结束后同步关键词到ViewModel:

// 可放到公共扩展文件
fun EditText.afterTextChanged(callback: (String) -> Unit) {
    addTextChangedListener(object : TextWatcher {
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        override fun afterTextChanged(s: Editable?) {
            callback(s.toString())
        }
    })
}

// Activity onCreate中初始化
binding.searchEt.afterTextChanged { query ->
    viewModel.updateSearchQuery(query)
}

状态收集

和生命周期绑定收集UI状态,控制列表和空提示的显隐:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            binding.progressBar.isVisible = state.isLoading
            // 空提示文案直接在布局中写死为“未找到”即可
            binding.emptyTipTv.isVisible = state.showEmptyTip
            binding.recyclerView.isVisible = !state.showEmptyTip && state.topicList.isNotEmpty()
            topicAdapter.submitList(state.topicList)
        }
    }
}

三、注意事项

  • 大小写转换必须指定Locale.getDefault(),不要调用无参的lowercase(),避免特殊语种(如土耳其语)的大小写转换异常
  • debounce时长可根据业务调整,200-300ms为体验最优区间,既不会出现输入卡顿,也不会触发过多无意义计算
  • 全量数据超过千条时必须保留flowOn(Dispatchers.Default)切换线程,否则过滤逻辑会阻塞主线程造成掉帧
  • 不要在状态流的转换逻辑里写埋点、页面跳转等副作用操作,保持状态计算纯逻辑

内容的提问来源于stack exchange,提问作者Compose Learner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 18:36:24