4 Comments

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