Search Bar in Compose — SearchBar, Filtering & Debounce
Material 3 SearchBar provides type-safe search with suggestions, filtering, and debounced ViewModel updates.
SearchBar with InputField
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val query by viewModel.query.collectAsState()
val suggestions by viewModel.suggestions.collectAsState()
val expanded = remember { mutableStateOf(false) }
SearchBar(
query = query,
onQueryChange = viewModel::onQueryChange,
onSearch = { expanded.value = false },
active = expanded.value,
onActiveChange = { expanded.value = it },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
placeholder = { Text("Search items...") },
leadingIcon = { Icon(Icons.Default.Search, null) },
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { viewModel.clearQuery() }) {
Icon(Icons.Default.Close, null)
}
}
}
) {
LazyColumn {
items(suggestions) { item ->
SuggestionItem(item, onSelect = {
viewModel.selectSuggestion(it)
expanded.value = false
})
}
}
}
}
Local Filtering with remember
@Composable
fun FilterChipDemo(items: List<String>) {
var query by remember { mutableStateOf("") }
val filtered = remember(query, items) {
items.filter { it.contains(query, ignoreCase = true) }
}
Column {
TextField(
value = query,
onValueChange = { query = it },
label = { Text("Filter") }
)
LazyColumn {
items(filtered) { item ->
Text(item, modifier = Modifier.padding(8.dp))
}
}
}
}
ViewModel Search with Debounce
class SearchViewModel @Inject constructor(
private val repository: ItemRepository
) : ViewModel() {
private val _query = MutableStateFlow("")
val query = _query.asStateFlow()
val suggestions = query
.debounce(300) // Wait 300ms after last keystroke
.distinctUntilChanged()
.flatMapLatest { q ->
if (q.isEmpty()) {
flowOf(emptyList())
} else {
repository.searchItems(q)
}
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun onQueryChange(newQuery: String) {
_query.value = newQuery
}
fun clearQuery() {
_query.value = ""
}
fun selectSuggestion(item: String) {
_query.value = item
}
}
FilterChip with SearchBar
@Composable
fun SearchWithFilters(viewModel: FilterViewModel = hiltViewModel()) {
val selectedFilter by viewModel.selectedFilter.collectAsState()
val filters = listOf("All", "Android", "Kotlin", "Compose")
Column {
SearchBar(
query = viewModel.query.collectAsState().value,
onQueryChange = viewModel::setQuery,
onSearch = { },
active = false,
onActiveChange = { }
)
LazyRow(modifier = Modifier.padding(8.dp)) {
items(filters) { filter ->
FilterChip(
selected = selectedFilter == filter,
onClick = { viewModel.setFilter(filter) },
label = { Text(filter) }
)
Spacer(modifier = Modifier.width(8.dp))
}
}
}
}
rememberSaveable for Rotation
@Composable
fun PersistentSearchBar() {
var query by rememberSaveable { mutableStateOf("") }
var active by rememberSaveable { mutableStateOf(false) }
SearchBar(
query = query,
onQueryChange = { query = it },
active = active,
onActiveChange = { active = it },
onSearch = { active = false }
)
}
Combine SearchBar + debounce + distinctUntilChanged for efficient, reactive search.
Top comments (0)