What You Will Build

A search bar with a leading search icon, a clearable text field, and a live-filtered list below. As the user types, the list filters instantly to show only matching items. The clear button appears only when there is text, and tapping it resets both the query and the filter.

Why This Pattern Matters

Client-side search filtering is essential for settings screens, contact lists, and any list longer than a few items. This pattern demonstrates real-time derived state, conditional icon rendering, and LazyColumn with filtered data.

The Search Bar Screen

@Composable
fun SearchBarScreen() {
    var query by remember { mutableStateOf("") }
    val allItems = remember {
        listOf("Apple", "Banana", "Cherry", "Date",
            "Elderberry", "Fig", "Grape", "Honeydew",
            "Kiwi", "Lemon", "Mango", "Nectarine",
            "Orange", "Papaya")
    }
    val filtered = allItems.filter {
        it.contains(query, ignoreCase = true)
    }

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Text("Search Bar",
            style = MaterialTheme.typography.headlineSmall,
            fontWeight = FontWeight.Bold)
        Spacer(Modifier.height(16.dp))

        OutlinedTextField(
            value = query,
            onValueChange = { query = it },
            modifier = Modifier.fillMaxWidth(),
            placeholder = { Text("Search...") },
            leadingIcon = { Icon(Icons.Default.Search, null) },
            trailingIcon = {
                if (query.isNotEmpty()) {
                    IconButton(onClick = { query = "" }) {
                        Icon(Icons.Default.Clear, "Clear")
                    }
                }
            },
            singleLine = true,
            shape = RoundedCornerShape(12.dp)
        )

        Spacer(Modifier.height(12.dp))

        LazyColumn(
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            items(filtered) { item ->
                Card(modifier = Modifier.fillMaxWidth()) {
                    Text(item,
                        modifier = Modifier.padding(16.dp),
                        style = MaterialTheme.typography.bodyLarge)
                }
            }
        }
    }
}

Tips and Pitfalls

  • ignoreCase = true in the filter ensures case-insensitive matching, which users expect.
  • RoundedCornerShape(12.dp) on the search field gives it a modern pill-like appearance.
  • Conditional trailingIcon: Only show the clear button when query is not empty to avoid confusing empty-state clicks.
  • For large datasets: Move filtering to a ViewModel with debounce using snapshotFlow and debounce() from kotlinx.coroutines.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.