Building a Searchable List in Jetpack Compose with SearchView
Last updated Jan 05, 2025In 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
remember
andmutableStateOf
for 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
Article Contributed By :
|
|
|
|
3409 Views |