如何使用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
相关产品推荐
相关产品推荐

