Creating a Search Field in Your Android app

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 a Uripattern 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:

Creating a Search Field in Your Android app

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.

For more complete information about compiler optimizations, see our Optimization Notice.