Global Search in Open Event Android

In the Open Event Android app we only had a single data source for searching in each page that was the content on the page itself. But it turned out that users want to search data across an event and therefore across different screens in the app. Global search solves this problem. We have recently implemented  global search in Open Event Android that enables the user to search data from the different pages i.e Tracks, Speakers, Locations etc all in a single page. This helps the user in obtaining his desired result in less time. In this blog I am describing how we implemented the feature in the app using JAVA and XML.

Implementing the Search

The first step of the work is to to add the search icon on the homescreen. We have done this with an id R.id.action_search_home.

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
   super.onCreateOptionsMenu(menu, inflater);
   inflater.inflate(R.menu.menu_home, menu);
   // Get the SearchView and set the searchable configuration
   SearchManager searchManager = (SearchManager)getContext().    getSystemService(Context.SEARCH_SERVICE);
   searchView = (SearchView) menu.findItem(R.id.action_search_home).getActionView();
  // Assumes current activity is the searchable activity
 searchView.setSearchableInfo(searchManager.getSearchableInfo(
getActivity().getComponentName()));
searchView.setIconifiedByDefault(true); 
}

What is being done here is that the search icon on the top right of the home screen  is being designated a searchable component which is responsible for the setup of the search widget on the Toolbar of the app.

@Override
 public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_home, menu);
    SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    searchView = (SearchView) menu.findItem(R.id.action_search_home).getActionView();
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));
 
    searchView.setOnQueryTextListener(this);
    if (searchText != null) {
        searchView.setQuery(searchText, true);
    }
    return true; }

We can see that a queryTextListener has been setup in this function which is responsible to trigger a function whenever a query in the SearchView changes.

Example of a Searchable Component

<?xml version="1.0" encoding="utf-8"?>
 <searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/global_search_hint"
    android:label="@string/app_name" />

For More Info : https://developer.android.com/guide/topics/search/searchable-config.html

If this searchable component is inserted into the manifest in the required destination activity’s body the destination activity is set and intent filter must be set in this activity to tell whether or not the activity is searchable.

Manifest Code for SearchActivity

<activity
        android:name=".activities.SearchActivity"
        android:launchMode="singleTop"
        android:label="Search App"
        android:parentActivityName=".activities.MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
    </intent-filter>
    <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable" />
 </activity>

And the attribute  android:launchMode=”singleTop is very important as if we want to search multiple times in the SearchActivity all the instances of our SearchActivity would get stored on the call stack which is not needed and would also eat up a lot of memory.

Handling the Intent to the SearchActivity

We basically need to do a standard if check in order to check if the intent is of type ACTION_SEARCH.

if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
    handleIntent(getIntent());
 }
@Override
 protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    handleIntent(intent);
 }
 public void handleIntent(Intent intent) {
    final String query = intent.getStringExtra(SearchManager.QUERY);
    searchQuery(query);
 }

The function searchQuery is called within handleIntent in order to search for the text that we received from the Homescreen.

SearchView Trigger Functions

Next we need to add two main functions in order to get the search working:

  • onQueryTextChange
  • onQueryTextSubmit

The function names are self-explanatory. Now we will move on to the code implementation of the given functions.

@Override
 public boolean onQueryTextChange(String query) {
    if(query!=null) {
        searchQuery(query);
        searchText = query;
    }
    else{
        results.clear();
        handleVisibility();
    }
   return true;
 }
 
 @Override
 public boolean onQueryTextSubmit(String query) {
    searchView.clearFocus();
    return true;
 }

The role of the searchView.clearFocus() inside the above code snippet is to remove the keyboard popup from the screen to enable the user to have a clear view of the search result.

Here the main search logic is being handled by the function called searchQuery which I’ll talking about now.

Search Logic

private void searchQuery(String constraint) {
 
    results.clear();
 
    if (!TextUtils.isEmpty(constraint)) {
        String query = constraint.toLowerCase(Locale.getDefault());
        addResultsFromTracks(query);
        addResultsFromSpeakers(query);
        addResultsFromLocations(query);
    }
 }
//THESE ARE SOME VARIABLES FOR REFERENCE
//This is the custom recycler view adapter that has been defined for the search
private GlobalSearchAdapter globalSearchAdapter;
 //This stores the results in an Object Array
 private List<Object> result

We are assuming that we have POJO’s(Plain Old Java Objects) for Tracks , Speakers , and Locations and for the Result Type Header.

The code posted below performs the function of getting the required results from the list of tracks. All the results are being fetched asynchronously from Realm and here we have also attached a header for the result type to denote whether the result is of type Track , Speaker or Location. We also see that we have added a changeListener to notify us if any changes have occurred in the set of results.

Similarly this is being done for all the result types that we need i.e Tracks, Locations and Speakers.

public void addResultsFromTracks(String queryString) {

   RealmResults<Track> filteredTracks = realm.where(Track.class)
                                        .like("name", queryString,                     Case.INSENSITIVE).findAllSortedAsync("name");
      filteredTracks.addChangeListener(tracks -> {

       if(tracks.size()>0){
            results.add("Tracks");
        }
       results.addAll(tracks);
       globalSearchAdapter.notifyDataSetChanged();
       Timber.d("Filtering done total results %d", tracks.size());
       handleVisibility();
});}

We now have a “Global Search” feature in the Open Event Android app. Users had asked for this feature and a learning for us is, that it would have been even better to do more tests with users when we developed the first versions. So, we could have included this feedback and implemented Global Search earlier on.

Resources