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.

Preview for app build in this article
App preview

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.


Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.