Building a Searchable List in Jetpack Compose with SearchView
Integrate SearchView with your Jetpack Compose listview. a step-by-step guide on building a dynamic search experience, including filtering data, updating the UI
In this Jetpack compose tutorial, we'll learn how to implement a SearchView with a filtered list in Jetpack Compose. The search view will allow users to type text, and the list below will be filtered in real-time based on the search query. Android SearchView facilitates a seamless search experience. Users can input text, and the list below instantly adjusts to reflect the entered query, providing a refined and relevant set of results.
Step 1: Create Data Model
First, let's create a data class to hold our list items
| data class Person( val id: Int, val name: String, val email: String ) |
Step 2: Create the Search View Component
Here's our main search view implementation:
|
@Composable OutlinedTextField( |
Step 3: Create the Filterable List Component
Now, let's create a component to display our filtered list
|
@Composable LazyColumn( @Composable |
Step 4: Implement the Main Screen
Here's how we put everything together in our main screen:
|
@Composable Column( // Search view // Filtered list |
Usage
To use this search functionality in your app:
- Add the required imports
|
import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardElevation import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp |
2. Set up your theme in your MainActivity:
| class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { YourAppTheme { SearchScreen() } } } } |
Features
This implementation includes:
- Real-time filtering as user types
- Clear button to reset search
- Smooth animations with LazyColumn
- Material Design styling
- Support for multiple search criteria (name and email)
- Responsive layout
- Clean, reusable components
Customization Options for Searchview implementation
You can customize the appearance by:
-
Modifying the card design
|
ElevatedCard ( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp), elevation = CardDefaults.cardElevation(), shape = RoundedCornerShape(8.dp,8.dp,8.dp,8.dp) ) |
2. Changing the search view style:
| SearchView( modifier = Modifier .fillMaxWidth() .padding(16.dp) .height(56.dp), hint = "Custom hint...", // Custom colors and shapes ) |
3. Adding animations:
| AnimatedVisibility( visible = query.isNotEmpty(), enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically() ) { // Filtered content } |
Best Practices
- State Management: Use
rememberandmutableStateOffor local state - Reusability: Create modular components that can be reused
- Performance: Use
remember(key)for expensive computations - User Experience: Provide clear feedback and smooth animations
- Error Handling: Handle empty states and loading states appropriately
Complete Code for Jetpack Compose Searchview implementation
|
import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardElevation import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp
// Data class for list items data class Person( val id: Int, val name: String, val email: String )
// Search view component
@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchView( modifier: Modifier = Modifier, hint: String = "Search...", onQueryChange: (String) -> Unit ) { var query by remember { mutableStateOf("") }
OutlinedTextField( value = query, onValueChange = { newQuery -> query = newQuery onQueryChange(newQuery) }, modifier = modifier .fillMaxWidth() .padding(16.dp), placeholder = { Text( text = hint, color = Color.Gray ) }, leadingIcon = { Icon( imageVector = Icons.Default.Search, contentDescription = "Search Icon", tint = Color.Gray ) }, trailingIcon = { if (query.isNotEmpty()) { IconButton( onClick = { query = "" onQueryChange("") } ) { Icon( imageVector = Icons.Default.Clear, contentDescription = "Clear Icon", tint = Color.Gray ) } } }, singleLine = true, shape = RoundedCornerShape(12.dp), colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colorScheme.primary, unfocusedBorderColor = Color.Gray ) ) }
// Filterable list component @Composable fun FilterableList( items: List, query: String ) { val filteredItems = remember(query, items) { if (query.isEmpty()) { items } else { items.filter { item -> item.name.contains(query, ignoreCase = true) || item.email.contains(query, ignoreCase = true) } } }
LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(filteredItems) { person -> PersonCard(person = person) } } }
// Card component for each person @Composable fun PersonCard(person: Person) { ElevatedCard ( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp), elevation = CardDefaults.cardElevation() ) { Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() ) { Text( text = person.name, style = MaterialTheme.typography.headlineSmall ) Spacer(modifier = Modifier.height(4.dp)) Text( text = person.email, style = MaterialTheme.typography.bodyMedium, color = Color.Gray ) } } }
// Main screen composable @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable fun SearchScreen() { var searchQuery by remember { mutableStateOf("") }
// Sample data val people = remember { listOf( Person(1, "John Doe", "john.doe@example.com"), Person(2, "Jane Smith", "jane.smith@example.com"), Person(3, "Bob Johnson", "bob.johnson@example.com"), Person(4, "Alice Brown", "alice.brown@example.com"), Person(5, "Charlie Wilson", "charlie.wilson@example.com") ) }
Column( modifier = Modifier .fillMaxSize() .background(Color.White) ) { // Top app bar TopAppBar( title = { Text("Search People") }, colors = TopAppBarColors( containerColor = MaterialTheme.colorScheme.primary, scrolledContainerColor = Color.White, navigationIconContentColor = Color.Green, titleContentColor = Color.White, actionIconContentColor = Color.White ),
)
// Search view SearchView( hint = "Search by name or email...", onQueryChange = { query -> searchQuery = query } )
// Filtered list FilterableList( items = people, query = searchQuery ) } }
// Optional: Add animations @Composable fun AnimatedFilterableList( items: List, query: String ) { val filteredItems = remember(query, items) { if (query.isEmpty()) { items } else { items.filter { item -> item.name.contains(query, ignoreCase = true) || item.email.contains(query, ignoreCase = true) } } }
LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(filteredItems) { person -> AnimatedVisibility( visible = true, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically() ) { PersonCard(person = person) } } } } |
Example 2 for How to create Searchview in Jetpack Compose LIstview
Step 1: Create android application in android studio
Step 2: Follow step for setup Jetpack Compose with Android Studio
In this example we are managing the data inside listview with Viewmodel and kotlin data classes
Lets create search Item Data
data class SearchData(var name: String? = null, var emailId: String? = null) |
View Model
package com.example.jetpack.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.jetpack.widget.SearchData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.*
import kotlin.collections.ArrayList
class SearchViewModel(appObj: Application) : AndroidViewModel(appObj) {
private val _searchList = MutableStateFlow(listOf())
val list = arrayListOf(
SearchData(
name = "Ankit Singh", emailId = "an@gmail.com"
),
SearchData(
name = "Neha Shaw", emailId = "ne@gmail.com"
),
SearchData(
name = "Arpita Ghosh", emailId = "ar@gmail.com"
),
SearchData(
name = "Akash Tiwari", emailId = "ak@gmail.com"
),
SearchData(
name = "Anisha Tiwari", emailId = "an@gmail.com"
),
SearchData(
name = "Rowdy Rathore", emailId = "ro@gmail.com"
),
SearchData(
name = "Jit Singh", emailId = "ji@gmail.com"
),
SearchData(
name = "Pravin Raj", emailId = "pr@gmail.com"
),
SearchData(
name = "Sneha Rao", emailId = "sn@gmail.com"
),
SearchData(
name = "Ranjana Rathore", emailId = "ran@gmail.com"
),
SearchData(
"Kamala Rathore", emailId = "ka@gmail.com"
)
)
val searchList: StateFlow> get() = _searchList
fun searchedItems(searchedText: String) {
if (searchedText.isNotEmpty()) {
val resultList = ArrayList()
for (data in list) {
if (data.name?.lowercase(Locale.getDefault())
?.contains(searchedText, ignoreCase = true) == true
) {
resultList.add(data)
}
}
_searchList.value = resultList
} else {
_searchList.value = list
}
}
init {
fetchList()
}
private fun fetchList() {
viewModelScope.launch {
_searchList.emit(list)
}
}
}
|
Here we have used stateFlow which emits list of serachData whenever search data will changed that can be observed while showing list items.
Lets create UI to display data inside listview
@Composable
fun SearchListItem(searchData: SearchData) {
Card(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth(),
elevation = 4.dp,
border = BorderStroke(width = 1.2.dp, Color.Blue)
) {
Row(content = {
Icon(
Icons.Default.Contacts,
"",
tint = Color.Blue,
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
)
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "${searchData.name}",
style = TextStyle(
color = Color.Blue,
fontSize = 21.sp,
fontWeight = FontWeight.Bold
)
)
Text(text = "${searchData.emailId}", modifier = Modifier.padding(top = 8.dp))
}
})
}
}
|
Let's create ui for SearchView ( we will create search view with prefix icon as magnifying glass, in the middle we will use text field and in the end cross icon) with SearchViewTextField composable function where we are going the pass the mutable state so that whenever user type any thing it will be save in state and according to typed data list will be rendered.
@Composable
fun SearchViewTextField(state: MutableState) {
Box(
modifier = Modifier
.border(width = 1.dp, color = Color.Gray, shape = CircleShape)
.fillMaxWidth()
) {
BasicTextField(
value = state.value,
onValueChange = {
state.value = it
},
modifier = Modifier
.background(Color.White, CircleShape)
.height(38.dp)
.fillMaxWidth(),
singleLine = true,
maxLines = 1,
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 10.dp)
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "image",
tint = Color.Blue
)
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.CenterStart
) {
if (state.value == TextFieldValue("")) Text(
"Search"
)
innerTextField()
}
if (state.value != TextFieldValue("")) {
IconButton(
onClick = {
state.value = TextFieldValue("")
},
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "image",
tint = Color.Blue
)
}
}
}
}
)
}
}
|
Create top App bar
TopAppBar(
title = { Text("Search View Demo") }
)
|
Lets combine Viewmodel, SearchView, List together and top app bar
@Composable
fun SearchViewDemo(searchViewModel: SearchViewModel) {
JetPackTheme(
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
content = {
SearchContent(searchViewModel)
}, topBar = {
TopAppBar(
title = { Text("Search View Demo") }
)
}
)
}
}
@Composable
fun SearchContent(searchViewModel: SearchViewModel) {
val searchList = searchViewModel.searchList.collectAsState()
val searchBy = remember { mutableStateOf(TextFieldValue("")) }
Column(
Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxSize()
) {
SearchViewTextField(searchBy)
searchViewModel.searchedItems(searchBy.value.text)
LazyColumn(
contentPadding = PaddingValues(
bottom = 100.dp
)
) {
items(
items = searchList.value,
itemContent = {
SearchListItem(searchData = it)
})
}
}
}
|
Complete example code to implement searchview to search the listview items with jetpack compose
package com.example.jetpack
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Contacts
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModelProvider
import com.example.jetpack.ui.theme.JetPackTheme
import com.example.jetpack.viewmodels.SearchViewModel
import com.example.jetpack.widget.SearchViewDemo
class MainActivity : ComponentActivity() {
@ExperimentalMaterialApi
@ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
val searchViewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
setContent {
SearchViewDemo(searchViewModel)
}
}
}
@ExperimentalMaterialApi
@ExperimentalFoundationApi
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
}
@Composable
fun SearchViewDemo(searchViewModel: SearchViewModel) {
JetPackTheme(
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
content = {
SearchContent(searchViewModel)
}, topBar = {
TopAppBar(
title = { Text("Search View Demo") }
)
}
)
}
}
@Composable
fun SearchContent(searchViewModel: SearchViewModel) {
val searchList = searchViewModel.searchList.collectAsState()
val searchBy = remember { mutableStateOf(TextFieldValue("")) }
Column(
Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxSize()
) {
SearchViewTextField(searchBy)
searchViewModel.searchedItems(searchBy.value.text)
LazyColumn(
contentPadding = PaddingValues(
bottom = 100.dp
)
) {
items(
items = searchList.value,
itemContent = {
com.example.jetpack.widget.SearchListItem(searchData = it)
})
}
}
}
@Composable
fun SearchListItem(searchData: SearchData) {
Card(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth(),
elevation = 4.dp,
border = BorderStroke(width = 1.2.dp, Color.Blue)
) {
Row(content = {
Icon(
Icons.Default.Contacts,
"",
tint = Color.Blue,
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
)
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "${searchData.name}",
style = TextStyle(
color = Color.Blue,
fontSize = 21.sp,
fontWeight = FontWeight.Bold
)
)
Text(text = "${searchData.emailId}", modifier = Modifier.padding(top = 8.dp))
}
})
}
}
@Composable
fun SearchViewTextField(state: MutableState) {
Box(
modifier = Modifier
.border(width = 1.dp, color = Color.Gray, shape = CircleShape)
.fillMaxWidth()
) {
BasicTextField(
value = state.value,
onValueChange = {
state.value = it
},
modifier = Modifier
.background(Color.White, CircleShape)
.height(38.dp)
.fillMaxWidth(),
singleLine = true,
maxLines = 1,
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 10.dp)
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "image",
tint = Color.Blue
)
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.CenterStart
) {
if (state.value == TextFieldValue("")) Text(
"Search"
)
innerTextField()
}
if (state.value != TextFieldValue("")) {
IconButton(
onClick = {
state.value = TextFieldValue("")
},
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "image",
tint = Color.Blue
)
}
}
}
}
)
}
}
data class SearchData(var name: String? = null, var emailId: String? = null)
|
Output for SearchView with Listview Items
![]() |
