How to Create a SearchView with Suggestions in Kotlin

It would be nice if creating a SearchView with suggestions were as simple as giving the SearchView a list of strings. But unfortunately, that’s not the case. It requires a CursorAdapter. This tells the SearchView how to render the suggestions and keeps track of the selection and what’s visible.

Search Suggestions

Creating the Menu and Item Layouts

If you’ve made a search menu before, this should look familiar. It’s a menu resource layout with a single item.


<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_search"
        android:title="@string/search"
        android:icon="@android:drawable/ic_menu_search"
        app:actionViewClass="androidx.appcompat.widget.SearchView"
        app:showAsAction="ifRoom"/>
</menu>

For the suggestion item, you can design it how you’d like–though a simple layout with a text view will suffice. The id needs to be given, and if there is more than one view where you want to attach data, each one should also have an id.


<layout
    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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        tools:context=".MainActivity">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/item_label"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="12dp"
            android:layout_marginBottom="8dp"
            android:textColor="@android:color/black"
            android:textStyle="bold"
            android:textSize="14sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Item" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Creating the Options Menu

Most of the initial menu setup, inside your fragment’s onCreateOptionsMenu is straightforward: inflate, findItem, get the SearchView, and apply query hint. The final line setting the threshold tells the SearchView when to call onQueryTextChange. In this case, we want it to start after a single character has been typed.


inflater.inflate(R.menu.search_menu, menu)

val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem?.actionView as SearchView

searchView.queryHint = getString(R.string.search)
searchView.findViewById<AutoCompleteTextView>(R.id.search_src_text).threshold = 1

To set up the CursorAdapter, we’ll need:

  1. An array of strings that represent column names. This is how the adapter knows what columns to look at when mapping data.
  2. An int array of ids. These are the ids that show where data should be assigned in the layout. Basically, you’re mapping from columns to views.
  3. The CursorAdapter. Provide the layout for the items, the “from” array, the “to” array, and a flag that lets the adapter know it should observe the content. You can also create a list of suggestions and attach them to the CursorAdapter.

val from = arrayOf(SearchManager.SUGGEST_COLUMN_TEXT_1)
val to = intArrayOf(R.id.item_label)
val cursorAdapter = SimpleCursorAdapter(context, R.layout.search_item, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER)
val suggestions = listOf("Apple", "Blueberry", "Carrot", "Daikon")

searchView.suggestionsAdapter = cursorAdapter

Now, we can set up the text listener. Submit only needs to hide the keyboard. For TextChange, we need to update the cursor with the possible suggestions. You can think of a MatrixCursor as a database, and as such, it takes columns. We previously provided the columns to the adapter for data and an id column, which is required by the SimpleCursorAdapter that was created above. Loop through the suggestions, and if one contains the query, add a row. Now, create a call to change the cursor.


searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    override fun onQueryTextSubmit(query: String?): Boolean {
        hideKeyboard()
        return false
    }

    override fun onQueryTextChange(query: String?): Boolean {
        val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1))
        query?.let {
            suggestions.forEachIndexed { index, suggestion ->
                if (suggestion.contains(query, true))
                cursor.addRow(arrayOf(index, suggestion))
            }
        }

        cursorAdapter.changeCursor(cursor)
        return true
    }
})

Finally, we need a suggestion listener. We only need to worry about the suggestion click. When a user clicks on a suggestion, it’s wise to hide the keyboard. Then we can get the current cursor from the adapter.

Think of this as the selected row in the database. From there, we can get the string value of the column we want. All that remains is doing something with the selection.


searchView.setOnSuggestionListener(object: SearchView.OnSuggestionListener {
    override fun onSuggestionSelect(position: Int): Boolean {
        return false
    }

    override fun onSuggestionClick(position: Int): Boolean {
        hideKeyboard()
        val cursor = searchView.suggestionsAdapter.getItem(position) as Cursor
        val selection = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1))
        searchView.setQuery(selection, false)

        // Do something with selection
        return true
    }
})

That finishes up the onCreateOptionsMenu just make sure to call setHasOptionsMenu(true) on your fragment, inside the fragment’s onActivityCreated will suffice.

The hideKeyboard method used above I have inside a UtilityExtentions file, that way they can be used in other parts of the app.


fun Context.hideKeyboard(view: View) {
    val inputMethodManager = getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
    inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
}

fun Fragment.hideKeyboard() {
    view?.let {
        activity?.hideKeyboard(it)
    }
}

The complete code can be found here kotlin-search

 
Conversation
  • Serge says:

    Hi, i tried to make this SearchView with Suggestions. But catched error

    ” android.view.InflateException: Binary XML file line #1: Binary XML file line #1: Error inflating class layout”.

    My AutoCompleteTextView have import from android.widget.AutoCompleteTextView.
    Where i can see all code from this tutorial?

    • Kory Dondzila Kory Dondzila says:

      Hey Serge, I’ve made some minor edits to the article and added a link at the end with the code. I’m not sure if you are having dependency issue or where AutoCompleteTextView is coming from.

  • Thanh Le says:

    Hi, i try to add images into simplecursoradapter but not work well suggestion cannot know it
    my example data
    countryList = mutableListOf(
    CountryItem(1,”Afghanistan”, R.drawable.afghanistan),
    CountryItem(2,”Bangladesh”, R.drawable.bangladesh),
    CountryItem(3,”China”, R.drawable.china),
    CountryItem(4,”India”, R.drawable.india),
    CountryItem(5,”Japan”, R.drawable.japan),
    CountryItem(6,”Nepal”, R.drawable.nepal),
    CountryItem(7,”North Korea”, R.drawable.nkorea),
    CountryItem(8,”South Korea”, R.drawable.skorea),
    CountryItem(9,”Srilanka”, R.drawable.srilanka),
    CountryItem(10,”Pakistan”, R.drawable.pakistan)
    )
    my adapter
    class CustomSimpleCursorAdapter(
    private val context: Context,
    layout: Int,
    private val c: Cursor?,
    from: Array,
    to: IntArray
    ) : SimpleCursorAdapter(context, layout, c, from, to) {

    override fun getView(pos: Int, inView: View, parent: ViewGroup): View {
    var v: View? = inView
    if (v == null) {
    val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    v = inflater.inflate(R.layout.custom_layout, null)
    }
    this.c!!.moveToPosition(pos)
    val titleStr = this.c.getString(this.c.getColumnIndex(“title”))
    val image = this.c.getBlob(this.c.getColumnIndex(“personImage”))
    val iv = v!!.findViewById(R.id.pic) as ImageView
    if (image != null) {
    // If there is no image in the database “NA” is stored instead of a blob
    // test if there more than 3 chars “NA” + a terminating char if more than
    // there is an image otherwise load the default
    if (image!!.size > 3) {
    iv.setImageBitmap(BitmapFactory.decodeByteArray(image, 0, image!!.size))
    } else {
    iv.setImageResource(R.drawable.ic_image_black_24dp)
    }
    }
    val fname = v!!.findViewById(R.id.label) as TextView
    fname.text = titleStr
    return v
    }
    }

    could you help me resolve this

    • Kory Dondzila Kory Dondzila says:

      Hey Thanh Le, sorry this took me a bit to get back to you about this. So a few things first make sure that your CustomSimpleCursorAdapter uses the androidx SimpleCursorAdapter “import androidx.cursoradapter.widget.SimpleCursorAdapter” and not the android one “import android.widget.SimpleCursorAdapter” Also make sure that you are passing in the flags Int as well, the SimpleCursorAdapter not using flags is deprecated.

      Because you are using drawable resources, you should be able to do a getIntOrNull on the cursor instead of getting the images themselves… though I don’t know what your CountryItem class looks like, I made the assumption is was a simple data class.

      I have provided a branch in my repo to showcase using images as you are doing it, roughly. If you were just using resources and didn’t have the behavior you needed, then the SimpleCursorAdapter alone should be enough.
      https://github.com/korydondzila/kotlin-search/tree/feature/using-images

  • Minensk says:

    Hey, thanks for the tutorial.
    But what do you mean by R.id.search_src_text ?
    Where do we reference this.

    • Kory Dondzila Kory Dondzila says:

      This is the ID for the SearchView menu item’s text view if I recall. It’s a default ID provided by that view.

  • Comments are closed.