This page looks best with JavaScript enabled

Android: SearchView with custom suggestions without ContentProvider

 ·  ☕ 8 min read  ·  ✍️ Iskander Samatov

In this (hopefully) short tutorial I’m going to provide step-by-step instructions on how to setup a SearchView which retrieves custom suggestions in real time from your back-end server. Reading Android official docs on this topic may seem somewhat confusing, especially if you’re not familiar with ContentProviders. So here I’m posting my solution which doesn’t include ContentProviders and is much simpler.

If you want to skip the details and get straight to the full code feel free to scroll down to the bottom, I’m posting the whole Activity implementation there. Otherwise, let’s begin!

Setup

Dependencies

In this tutorial I’m going to use RxJava2 and Retrofit libraries to handle asynchronous calls to my server. If you’re not yet, I highly recommend using them in your Android development.

Make sure to include support libraries in your project’s gradle file. The latest version at the time of writing is 28:

  • implementation 'com.android.support:appcompat-v7:28.0.0'
  • implementation 'com.android.support:support-v4:28.0.0'

Activity

The activity must either implement custom toolbar or use Theme.AppCompat.Lighttheme or any other theme that has an embedded toolbar.

The process

1. Create action menu file for your toolbar

In the res folder inside your project create a new folder called menu. Inside the folder, create a new xml file called search_menu.xml This file will contain quick action items for your toolbar. Here’s what it looks like: [xml]

[/xml]

Important notes :

  • showAsAction  This property is set so that the item will collapse into three dot menu if there’s no enough space in the toolbar
  • actionViewClass this property is important. It specifies the view that will be presented when the item is clicked
  • icon I used a simple vector image for the icon

2. Override onCreateOptionsMenu in your activity

First let me show you the whole method and then I’ll explain it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
     getMenuInflater().inflate(R.menu.search_action_bar_menu, menu);
     Activity activity = this;
     SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
     SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
     searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
     searchView.setQueryHint("Search for users…");
     String[] columNames = {
         SearchManager.SUGGEST_COLUMN_TEXT_1
     };
     int[] viewIds = {
         android.R.id.text1
     };
     CursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, null, columNames, viewIds);
     searchView.setSuggestionsAdapter(adapter);
     searchView.setOnSuggestionListener(getOnSuggestionClickListener());
     searchView.setOnQueryTextListener(getOnQueryTextListener(activity, adapter));
     return true;
 }
  • Line 3 adds action menu items you created earlier to your toolbar.
  • Lines 6 to 13 create references to SearchManager  and SearchView components and set up necessary properties, like query hint.
  • Line 14 creates a list of columns used from the result to populate the suggestion list, in this example we’re only going to display user’s username
  • Line 15 creates a list of ids of the views which will be populated with the result rows, in this example we’re using list items provided by the android resource library.
  • Lines 16 and 17 create a CursorAdapter which is used by the SearchView  to display the suggestions. We’re using SimpleCursorAdapter provided by the Android.
  • Finally lines 21 and 22 setup listeners which are used to trigger the events.

3. Setup listeners

Now let’s talk about getOnSuggestionClickListener() and getOnQueryTextListener(activity, adapter) methods which return the listeners that are used by the SearchView . I’ll start with getOnQueryTextListener. This is a listener that gets triggered every time the user types something in the SearchView. This is what it looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private SearchView.OnQueryTextListener getOnQueryTextListener(Activity activity, CursorAdapter adapter) {
private SearchView.OnQueryTextListener getOnQueryTextListener(Activity activity, CursorAdapter adapter) {
     return new SearchView.OnQueryTextListener() {
         @Override
         public boolean onQueryTextSubmit(String s) {
             return false;
         }
         @Override public boolean onQueryTextChange(String s) {
             if (s.length() == 2) {
                 return false;
             }
             Observable < map < string, user = "" >> observable = SearchMiddleware.searchForUsers(s, activity);
             observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(getRequestObserver(adapter));
             return true;
         }
     };
 }

The method returns a new instance of the SearchView.OnQueryTextListener which implements two methods:

  • onQueryTextSubmit which is triggered when the user clicks on the submit button in the SearchView
  • onQueryTextChange which gets triggered when the user starts typing.

In this example we’re only going to worry about onQueryTextChange to provide the users with the real-time suggestions as they type. onQueryTextChange method uses the RxJava to make an asynchronous call to my back-end server and subscribes to it on the main thread using AndroidSchedulers.mainThread().

Again if you’re not familiar with RxJava I highly recommend using, I’ve only started using it recently and it already helped me avoid tons of boilerplate code. It’s the best library so far to deal with Android’s rule about not blocking the main thread.

I don’t know about you but I don’t like long methods. So I created a separate method for getting the Observer which receives the results of the query on the main thread. Here’s what it looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private Observer getRequestObserver(CursorAdapter adapter) {
     return new Observer > () {
         @Override
         public void onSubscribe(Disposable d) {}
         @Override
         public void onNext(Map < string, user = "" > users) {
             Cursor cursor = createCursorFromResult(users);
             adapter.swapCursor(cursor);
         }
         @Override
         public void onError(Throwable e) {}
         @Override
         public void onComplete() {}
     };
 }

Don’t worry if you’re not familiar with the Observer implementation, it’s a part of RxJava. The important part here is onNext method. This method creates a Cursor and assigns it to the CursorAdapter we created earlier using swapCursor. Calling swapCursor will magically make the SearchView display the result of the query.

Following my principle of splitting long methods I wrote a separate createCursorFromResult(users) method which creates a Cursor object from the users result. Here’s the implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private Cursor createCursorFromResult(Map users) {
     String[] menuCols = new String[] {
         BaseColumns._ID,
             SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_INTENT_DATA
     };
     MatrixCursor cursor = new MatrixCursor(menuCols);
     int counter = 0;
     for (String username: users.keySet()) {
         User user = users.get(username);
         cursor.addRow(new Object[] {
             counter,
             user.getUsername(),
             user.getUsername()
         });
         counter++;
     }
     return cursor;
 }

This method creates an array of columns which will be present in the Cursor using static constants provided by the Android. BaseColumns._ID is mandatory in any cursor, the other two are required for our implementation. Then we create a new instance of aMatrixCursor and pass the column array in the constructor. Finally, we iterate over the query result and for each item we insert a new row in the cursor.

The only thing left is getOnSuggestionClickListener() method which returns a listener that gets triggered whenever the user clicks on the suggestions in the SearchView:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private SearchView.OnSuggestionListener getOnSuggestionClickListener() {
    return new SearchView.OnSuggestionListener() {
    private SearchView.OnSuggestionListener getOnSuggestionClickListener() {
    return new SearchView.OnSuggestionListener() {
        @Override
        public boolean onSuggestionSelect(int i) {
            return false;
        }
        @Override
        public boolean onSuggestionClick(int index) { 
            // TODO: handle suggestion item click             
            return true;         }     
    };
}

This method returns a new instance of SearchView.OnSuggestionListener which is triggered whenever the users clicks on the suggestion. As you can see I left the implementation of the listener methods for you to finish as you see fit.

As promised here’s the full implementation of the Activity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class DemoActivity extends AppCompatActivity {
     @Override protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_demo);
     }
     @Override public boolean onCreateOptionsMenu(Menu menu) {
         getMenuInflater().inflate(R.menu.search_action_bar_menu, menu);
         Activity activity = this;
         SearchManager searchManager =
             (SearchManager) getSystemService(Context.SEARCH_SERVICE);
         SearchView searchView =
             (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
         searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
         searchView.setQueryHint("Search for users…");
         String[] columNames = {
             SearchManager.SUGGEST_COLUMN_TEXT_1
         };
         int[] viewIds = {
             android.R.id.text1
         };
         CursorAdapter adapter =
             new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1,
                 null, columNames, viewIds);
         searchView.setSuggestionsAdapter(adapter);
         searchView.setOnSuggestionListener(getOnSuggestionClickListener());
         searchView.
         setOnQueryTextListener(getOnQueryTextListener(activity, adapter));
         return true;
     }
     private SearchView.
     OnQueryTextListener getOnQueryTextListener(Activity activity,
         CursorAdapter adapter) {
         return new SearchView.OnQueryTextListener() {
             @Override public boolean onQueryTextSubmit(String s) {
                 return false;
             }
             @Override public boolean onQueryTextChange(String s) {
                 if (s.length() == 2) {
                     return false;
                 }
                 Observable < map < string, user = "" >> observable =
                     SearchMiddleware.searchForUsers(s, activity);
                 observable.subscribeOn(Schedulers.io()).
                 observeOn(AndroidSchedulers.mainThread()).
                 subscribe(getRequestObserver(adapter));
                 return true;
             }
         };
     }
     private Observer getRequestObserver(CursorAdapter adapter) {
         return new Observer < map < string, user = "" >> () {
             @Override public void onSubscribe(Disposable d) {}
             @Override public void onNext(Map < string, user = "" > users) {
                 Cursor cursor = createCursorFromResult(users);
                 adapter.swapCursor(cursor);
             }
             @Override public void onError(Throwable e) {}
             @Override public void onComplete() {}
         };
     }
     private SearchView.OnSuggestionListener getOnSuggestionClickListener() {
         return new SearchView.OnSuggestionListener() {
             @Override public boolean onSuggestionSelect(int i) {
                 return false;
             }
             @Override public boolean onSuggestionClick(int index) {
                 // TODO: handle suggestion item click             
                 return true;
             }
         };
     }
     private Cursor createCursorFromResult(Map < string, user = "" > users) {
         String[] menuCols = new String[] {
             BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1,
                 SearchManager.SUGGEST_COLUMN_INTENT_DATA
         };
         MatrixCursor cursor = new MatrixCursor(menuCols);
         int counter = 0;
         for (String username: users.keySet()) {
             User user = users.get(username);
             cursor.addRow(new Object[] {
                 counter,
                 user.getUsername(),
                 user.getUsername()
             });
             counter++;
         }
         return cursor;
     }
 }

That’s the end of this short tutorial. Hopefully you were able to follow along and set up the SearchView in your own app.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.