Nelson Glauber
Senior Software Engineer at CESAR
Search is a feature existing in a large number of Android applications. Providing a way to search content inside your app is very important as it helps users to find the content that they want.
However, this search must be fast and efficient as getting information may be the main reason the user opened your app. The Android SDK provides a set of APIs to implement this pattern in your app, and in this post we'll review the first steps needed to implement it into your application.
Hands on!
Let's get started! Create a new project in Android Studio using the "Empty activity" template. Once the project is created, add a new menu file and name it as res/menu/menu_search.xml
:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/search"
android:title="@string/hint_search"
android:icon="@android:drawable/ic_menu_search"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>
Our menu file contains just one item responsible to display a search button. When the user presses this button, it expands and shows a text field allowing the use to enter the term to be searched. The widget responsible for this task is the SearchView
as we can see in the app:actionViewClass
attribute. Definingapp:showAsAction
with the value collapseActionView
allows SearchView
to expand itself when the button is tapped.
Notice we are using the support library here to keep compatibility across Android versions.
Once we create a menu file, we're going to load it in the MainActivity
like described below:
public class MainActivity extends AppCompatActivity
implements SearchView.OnQueryTextListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_search, menu);
MenuItem searchItem = menu.findItem(R.id.search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
searchView.setOnQueryTextListener(this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
// User pressed the search button
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
// User changed the text
return false;
}
}
Inside onCreateOptionsMenu()
we are loading the menu resource file using inflate()
. Through theMenu
object we are finding the MenuItem
that contains the SearchView
widget. Setting anOnQueryTextListener
object to the SearchView
our app can detect two events:
onQueryTextChange
is called when the user types each character in the text field;onQueryTextSubmit
is triggered when the search is pressed.
So, you could implement whatever you want in these methods to perform a search (access a SQLite database or a web service, for example). That's it! This is the easiest way to implement a search functionality in your app.
Improving searching experience
The previous approach works fine, but in general a search operation is needed to help users find what they want. A good way to do this is showing some suggestions while the user is typing. To demonstrate this approach, we will display a list of cities while the user types the city's name in the SearchView
. This list is stored in a server. The first thing we have to do is create a searchable configuration. So, add a new file to the project at res/xml/searchable.xml
:
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/hint_search"
android:label="@string/app_name"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
android:searchSuggestAuthority="ngvl.android.demosearch.citysuggestion"
android:searchSuggestIntentAction="android.intent.action.VIEW"
android:searchSuggestIntentData="content://ngvl.android.demosearch.city"/>
Let's understand the properties defined in this file:
- Voice search button was enabled using
android:voiceSearchMode
property. - While the user is typing, the
query()
method will be called in a content provider that responds toandroid:searchSuggestAuthority
(we'll see this provider just later). - When the user selects a suggestion in the list, a new activity will be called using the action described in
android:searchSuggestIntentAction
property. android:searchSuggestIntentData
complements the previous property because it defines aUri
pattern for the Intent triggered when the user clicks in a suggestion.
As we know, content providers usually accesses a SQLite database to store and retrieve data, but we can use it to access the web too, as the Android documentation says clearly:
Content providers "may be called from many threads at once, and must be thread-safe".
In this example, the suggestions will be retrieved from a JSON file stored in a web server. Create a new content provider naming it CitySuggestionProvider
and just implement query()
(the others are empty):
public class CitySuggestionProvider extends ContentProvider {
List<String> cities;
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (cities == null || cities.isEmpty()){
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://dl.dropboxusercontent.com/u/6802536/cidades.json")
.build();
try {
Response response = client.newCall(request).execute();
String jsonString = response.body().string();
JSONArray jsonArray = new JSONArray(jsonString);
cities = new ArrayList<>();
int lenght = jsonArray.length();
for (int i = 0; i < lenght; i++) {
String city = jsonArray.getString(i);
cities.add(city);
}
} catch (Exception e) {
e.printStackTrace();
}
}
MatrixCursor cursor = new MatrixCursor(
new String[] {
BaseColumns._ID,
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
}
);
if (cities != null) {
String query = uri.getLastPathSegment().toUpperCase();
int limit = Integer.parseInt(uri.getQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT));
int lenght = cities.size();
for (int i = 0; i < lenght && cursor.getCount() < limit; i++) {
String city = cities.get(i);
if (city.toUpperCase().contains(query)){
cursor.addRow(new Object[]{ i, city, i });
}
}
}
return cursor;
}
//
Other methods are empty...
}
When the user starts to type the text, our provider is called using an uri like below.content://ngvl.android.demosearch.citysuggestion/search_suggest_query/santos?limit=50
The system calls our provider adding search_suggest_query/<text>
in the end of the uri. A limit of results is set to 50 by default, and it is passed as a query parameter. It's important to note that the search operation is done outside the UI thread so we can perform a HTTP request like we did. In this example we are storing the city's list in memory, but in practice we can use a table in our SQLite database.
Using the Uri
object, we've got the text parameter using getLastPathSegment()
and max number of suggestions to be returned using getQueryParameter()
. After reading the content from the web, a MatrixCursor was built to represent the suggested cities list. One important detail in here is column names. The columns "_id" and "suggest_text_1" are obligatory. You can use more columns or create a subclass of cursor adapter to customize your suggestions list.
As I said before, our suggestions are stored in a JSON file on a web server. So here, we are performing the request to retrieve the cities' list and reading it as a JSON file using OkHttp library. So, don't forget to add this dependency in the build.gradle
file:
dependencies {
...
compile 'com.squareup.okhttp3:okhttp:3.0.1'
}
Be sure to add INTERNET permission in AndroidManifest.xml
and the content provider is declared like below:
<manifest ...
<uses-permission android:name="android.permission.INTERNET"/>
<application...
<provider
android:name=".CitySuggestionProvider"
android:authorities="ngvl.android.demosearch.citysuggestion"
android:enabled="true"
android:exported="true"/>
Be sure the android:authorities
is the same as declared in the searchable configuration file.
After that, we also need to connect the activity with the searchable configuration. Change the activity declaration in AndroidManifest.xml
:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
Using the value android.app.searchable
for the <meta-data>
tag, we are defining the activity has a searchable configuration. Now we have set this information to the SearchView
widget. Modify the onCreateOptionsMenu()
to look like below:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_search, menu);
MenuItem searchItem = menu.findItem(R.id.search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
searchView.setOnQueryTextListener(this);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchView.setSearchableInfo(searchManager.getSearchableInfo(
new ComponentName(this, MainActivity.class)));
searchView.setIconifiedByDefault(false);
return true;
}
Run the application and start typing some text inside the SearchView. You'll see that some suggestions are displayed:
Nice! Our suggestions feature is working. But what happens when a suggestion is chosen? Or if I pressed the search button in the keyboard?
Handling suggestions and search action
At this moment, when the user presses the search button or selects a suggestion in the list, a new instance of the MainActivity is created and displayed. To avoid this, we can set our activity's launch mode assingleTop
in AndroidManifest.xml
and implement onNewIntentMethod()
:
<activity android:name=".MainActivity"
android:launchMode="singleTop">
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
Toast.makeText(this, "Searching by: "+ query, Toast.LENGTH_SHORT).show();
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
String uri = intent.getDataString();
Toast.makeText(this, "Suggestion: "+ uri, Toast.LENGTH_SHORT).show();
}
}
When the user clicks on the search button, the activity is called using ACTION_SEARCH, but when selecting a suggestion, the ACTION_VIEW is used (as we defined in searchable configuration file). If you run the application now and perform a search, the same instance of MainActivity
is used instead of creating a new one.
You can use this approach without problem, but you could need (or prefer) to perform the search in another activity. To do that, we are going to create a new searchable activity that will be called when the user presses the search button or selects a suggestion.
Its declaration in AndroidManifest.xml
must be like this:
<activity android:name=".SearchableActivity">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
Remember to remove these same configurations (<intent-filter>
and <meta-data>
) from theMainActivity
declaration. So now, we have to perform a little change in the MainActivity
.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
...
searchView.setSearchableInfo(searchManager.getSearchableInfo(
new ComponentName(this, SearchableActivity.class)));
...
}
// Remove onNewIntent method too
With this implementation, when the user presses the return/search button, a new activity is started using the action "android.intent.action.SEARCH
" and one parameter called "query
". This parameter represents the text typed by the user in the SearchView
. The code below shows how to handle a search in the SearchableActivity
:
public class SearchableActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_searchable);
TextView txt = (TextView)findViewById(R.id.textView);
Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
txt.setText("Searching by: "+ query);
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
String uri = intent.getDataString();
txt.setText("Suggestion: "+ uri);
}
}
}
Once you have a query parameter you can search whatever you want. For example, a local database/content provider or in a web service.
Conclusion
In this post, we implemented a search feature in an Android app and saw how easy it is. These are just the first steps, for more details check Android Search training tutorial.
The source code of this sample is available at my GitHub.