Hi there! Welcome to my blog!
This article teaches you to implement your own Search Interface which takes user submitted Query and performs search on list and returns newly filtered list.
Query can be submitted by typing in EditText or by voice to speech functionality.
We will not use SearchView or Searchable configuration as they are old approach and does not fit with fragments just like activities. We use EditText for user to submit query and return new list after executing simple search operation, let’s start with outline.
1. Start with new android studio project.
Start with new android studio project choosing Empty Activity.
Give project and package name and select language Kotlin and finish.
You can see the source code from my Github Repository.
Java and Kotlin code both are added to repository in different branches. master and simple-search branches are kotlin, other one is java.
2. Add dependencies and resources.
Recheck if you have included these dependencies without missing them.
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
Use the below resources replacing default color and theme.
Colors.xml :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="blue_200">#448AFF</color>
<color name="blue_dark_primary">#141A20</color>
<color name="blue_light_variant">#1D262E</color>
</resources>
3. Create RecyclerView and Adapter.
Let’s create a model class with single property title of type String, you can add more properties later on.
Sports.kt:
data class Sports(
val title: String
)
Create layout file for showing each item in list.
item_sport.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingVertical="16dp">
<TextView
android:id="@+id/search_title_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="sans-serif-smallcaps"
android:textAppearance="?attr/textAppearanceHeadline5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Basketball" />
</androidx.constraintlayout.widget.ConstraintLayout>
Now create RecyclerView adapter class which takes list of sports property as parameter.
SportsAdapter.kt:
class SearchAdapter(
private val sportsList: List<Sports>
) : RecyclerView.Adapter<SearchAdapter.SearchViewHolder>() {
class SearchViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.search_title_text_view)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
return SearchViewHolder(LayoutInflater.from(parent.context).inflate(
R.layout.item_search, parent, false)
)
}
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
val sports: Sports = sportsList[position]
holder.titleTextView.text = sports.title
}
override fun getItemCount() = sportsList.size
}
Data to show in adapter, create a kotlin file and add this function which returns list of strings.
SportsData.kt:
fun sportsList(): List<Sports> {
val sportsList = ArrayList<Sports>()
listOf(
"Rugby", "Cricket", "Basketball", "Hockey", "Volleyball", "Esports", "Kabaddi",
"Baseball", "MMA", "Soccer", "Handball", "Tennis",
).forEach {
sportsList.add(Sports(it))
}
return sportsList
}
We have data and adapter with layout ready, let’s attach adapter to activity and design the search layout.
4. Design search layout box with EditText.
The below search box container is not child of AppbarLayout or Toolbar. We simply apply margin to parent CardView and add childrens to it. Below that, we add RecyclerView as sibling and root parent ConstraintLayout.

First let’s create layout for search box.
search_header_layout:
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="16dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/search_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:src="@android:drawable/ic_menu_search"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/clear_search_query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackground"
android:padding="8dp"
android:src="@drawable/ic_clear"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/voice_search_query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackground"
android:padding="8dp"
android:src="@android:drawable/ic_btn_speak_now"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/search_edit_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:background="@android:color/transparent"
android:hint="@string/search_articles_hint"
android:inputType="text"
android:paddingTop="8dp"
android:textAppearance="?attr/textAppearanceHeadline6"
app:layout_constraintBottom_toBottomOf="@id/search_image_view"
app:layout_constraintEnd_toStartOf="@id/voice_search_query"
app:layout_constraintStart_toEndOf="@id/search_image_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
Now let’s include that into activity xml file along with RecyclerView. We also add one more TextView, it shows when no search results are found by matching our query.
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<include
android:id="@+id/search_box_container"
layout="@layout/search_header_layout" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_box_container"
tools:itemCount="12"
tools:listitem="@layout/item_search" />
<TextView
android:id="@+id/no_search_results_found_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_search_results_found"
android:textAppearance="?attr/textAppearanceHeadline4"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/search_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/search_list" />
</androidx.constraintlayout.widget.ConstraintLayout>
5. Show list data in activity to perform search.
Attach adapter to activity class.
ActivityMain.kt:
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var noSearchResultsFoundText: TextView
private val sportsList: List<Sports> = sportsList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.search_list)
noSearchResultsFoundText = findViewById(R.id.no_search_results_found_text)
attachAdapter(sportsList)
toggleRecyclerView(sportsList)
}
private fun attachAdapter(list: List<Sports>) {
val searchAdapter = SearchAdapter(list)
recyclerView.adapter = searchAdapter
}
private fun toggleRecyclerView(sportsList: List<Sports>) {
if (sportsList.isEmpty()) {
recyclerView.visibility = View.INVISIBLE
noSearchResultsFoundText.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
noSearchResultsFoundText.visibility = View.INVISIBLE
}
}
}
6. Search user entered query using listener.
Now let’s grab the text query that user entered in EditText and call text listener for listening the changes.
Each time the text changes we perform search operation to the list.
Let’s initialize the EditText and add text change listener and filter the string.
private lateinit var editText: AppCompatEditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
editText = findViewById(R.id.search_edit_text)
editText.doOnTextChanged { text, _, _, _ ->
val query = text.toString().toLowerCase(Locale.getDefault())
filterWithQuery(query)
}
}
You can also use TextWatcher( ) if doOnTextChanged { } extension is not available.
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Do Nothing
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val query = text.toString().toLowerCase(Locale.getDefault())
filterWithQuery(query)
}
override fun afterTextChanged(s: Editable?) {
// Do Nothing
}
})
Create function filterWithQuery(query: String)
If the query is empty, don’t do search operation.
If the query is valid, create a new arrayList to store valid strings. But for this we need a function which takes query and returns the new arrayList when everytime the query changes.
We can start by naming it as onQueryChanged(filterQuery: String) and returns new array list.
// Takes user submitted query and returns filtered array list.
private fun onQueryChanged(filterQuery: String): List<Sports> {
// Empty new array list which contains new strings
val filteredList = ArrayList<Sports>()
// Loop through each item in list
for (currentSport in sportsList) {
// Before checking string matching convert string to lower case.
if (currentSport.title.toLowerCase(Locale.getDefault()).contains(filterQuery)) {
// If the match is success, add item to list.
filteredList.add(currentSport)
}
}
return filteredList
}
private fun filterWithQuery(query: String) {
// Perform operation only is query is not empty
if (query.isNotEmpty()) {
// Call the function with valid query and take new filtered list.
val filteredList: List<Sports> = onQueryChanged(query)
// Call the adapter with new filtered array list
attachAdapter(filteredList)
// If the matches are empty hide RecyclerView and show error text
toggleRecyclerView(filteredList)
} else if (query.isEmpty()) {
// If query is empty don't make changes to list
attachAdapter(sportsList)
}
}
7. Enable voice search capabilities.
Let’s make the voice ImageView works, it should listen to user speech and submit it as query. Also add click listener to view, when user clicks it, start a intent of type RecognizerIntent.ACTION_RECOGNIZE_SPEECH with result activity like below.
private lateinit var clearQueryImageView: ImageView
private lateinit var voiceSearchImageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
clearQueryImageView = findViewById(R.id.clear_search_query)
voiceSearchImageView = findViewById(R.id.voice_search_query)
editText.doOnTextChanged { text, _, _, _ ->
val query = text.toString().toLowerCase(Locale.getDefault())
toggleImageView(query)
}
voiceSearchImageView.setOnClickListener {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
}
startActivityForResult(intent, MainActivity.SPEECH_REQUEST_CODE)
}
clearQueryImageView.setOnClickListener {
editText.setText("")
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == MainActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val spokenText: String? =
data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS).let { results ->
results?.get(0)
}
// Do something with spokenText
editText.setText(spokenText)
}
super.onActivityResult(requestCode, resultCode, data)
}
// If query is empty, hide clear view and show voice control imageView
private fun toggleImageView(query: String) {
if (query.isNotEmpty()) {
clearQueryImageView.visibility = View.VISIBLE
voiceSearchImageView.visibility = View.INVISIBLE
} else if (query.isEmpty()) {
clearQueryImageView.visibility = View.INVISIBLE
voiceSearchImageView.visibility = View.VISIBLE
}
}
companion object {
const val SPEECH_REQUEST_CODE = 0
}
Your Activity class should look like below after making all these changes.
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var noSearchResultsFoundText: TextView
private val sportsList: List<Sports> = sportsList()
private lateinit var editText: AppCompatEditText
private lateinit var clearQueryImageView: ImageView
private lateinit var voiceSearchImageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.search_list)
noSearchResultsFoundText = findViewById(R.id.no_search_results_found_text)
editText = findViewById(R.id.search_edit_text)
clearQueryImageView = findViewById(R.id.clear_search_query)
voiceSearchImageView = findViewById(R.id.voice_search_query)
attachAdapter(sportsList)
editText.doOnTextChanged { text, _, _, _ ->
val query = text.toString().toLowerCase(Locale.getDefault())
filterWithQuery(query)
toggleImageView(query)
}
voiceSearchImageView.setOnClickListener {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
}
startActivityForResult(intent, SPEECH_REQUEST_CODE)
}
clearQueryImageView.setOnClickListener {
editText.setText("")
}
}
private fun attachAdapter(list: List<Sports>) {
val searchAdapter = SearchAdapter(list)
recyclerView.adapter = searchAdapter
}
private fun filterWithQuery(query: String) {
if (query.isNotEmpty()) {
val filteredList: List<Sports> = onQueryChanged(query)
attachAdapter(filteredList)
toggleRecyclerView(filteredList)
} else if (query.isEmpty()) {
attachAdapter(sportsList)
}
}
private fun onQueryChanged(filterQuery: String): List<Sports> {
val filteredList = ArrayList<Sports>()
for (currentSport in sportsList) {
if (currentSport.title.toLowerCase(Locale.getDefault()).contains(filterQuery)) {
filteredList.add(currentSport)
}
}
return filteredList
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val spokenText: String? =
data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS).let { results ->
results?.get(0)
}
// Do something with spokenText
editText.setText(spokenText)
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun toggleRecyclerView(sportsList: List<Sports>) {
if (sportsList.isEmpty()) {
recyclerView.visibility = View.INVISIBLE
noSearchResultsFoundText.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
noSearchResultsFoundText.visibility = View.INVISIBLE
}
}
private fun toggleImageView(query: String) {
if (query.isNotEmpty()) {
clearQueryImageView.visibility = View.VISIBLE
voiceSearchImageView.visibility = View.INVISIBLE
} else if (query.isEmpty()) {
clearQueryImageView.visibility = View.INVISIBLE
voiceSearchImageView.visibility = View.VISIBLE
}
}
companion object {
const val SPEECH_REQUEST_CODE = 0
}
}
If you run the app, you should notice everything working properly, if not take reference from source code below I have shared.
If you need java code check other branches in the same repository.
8. Project code and helpful resources.
With reference to this I will also create articles, examples on implementing search in database and JSON.
That’s it guys, thanks for reading and do follow my GitHub and LinkedIn from below.

Rajasekhar K E
Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.
Here We Go Again : (
if (article == helpful) {
println("Like and subscribe to blog newsletter.")
} else {
println("Let me know what i should blog on.")
}
You must be logged in to post a comment.