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.Light
theme or any other theme that has an embedded toolbar.
The process
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
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!