All Posts

In this tutorial I will present you how to use the SearchView using FTS3.

  • If your data is stored in a SQLite database on the device, performing a full-text search (using FTS3, rather than a LIKE query) can provide a more robust search across text data and can produce results significantly faster. See sqlite.org for information about FTS3 and the SQLiteDatabase class for information about SQLite on Android. Also look at the Searchable Dictionary sample application to see a complete SQLite implementation that performs searches with FTS3. (http://developer.android.com/guide/topics/search/search-dialog.html)

In order to do this we will make a ListView which will contain a list of names and the search will be placed above the list. The behavior of our search will be this:

– the user has a list of some data and wants to perform a search
– the user types in the search field what he wants to find
– as the user types in the search field a new list with suggestions will be displayed
– the user selects an option from the search list
– the previous list (the one with all data) will be scrolled to the searched item
Now let’s implement the code 🙂
1. Create a new project and rename your automatically created activity to “MyActivity”
2. Now we have to create the listView.
  • Go to res-layout-main.xml and copy the code below:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

    <SearchView android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/search"/>

    <ListView android:id="@+id/list"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:transcriptMode="alwaysScroll"
              android:cacheColorHint="#00000000"
              android:listSelector="@android:color/transparent"/>
</LinearLayout>
  • Create a new xml file for the row of the list. Name it list_item.xml and copy the following code:
    <?xml version="1.0" encoding="utf-8"?>
    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:id="@+id/list_item"
            android:gravity="center_vertical">
    
    
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/list_item_text_view"
                  android:textSize="20sp"
                  android:padding="10dp"
                  android:layout_marginLeft="5dp"/>
    
    </LinearLayout>
  • Create the adapter for the list. To do this create a new java class in the package where MyActivity is and name it MyCustomAdapter and copy the code below:
package com.example;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import java.util.ArrayList;


public class MyCustomAdapter extends BaseAdapter {
    private ArrayList<String> mListItems;
    private LayoutInflater mLayoutInflater;

    public MyCustomAdapter(Context context, ArrayList<String> arrayList){

        mListItems = arrayList;

        //get the layout inflater
        mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public int getCount() {
        //getCount() represents how many items are in the list
        return mListItems.size();
    }

    @Override
    //get the data of an item from a specific position
    //i represents the position of the item in the list
    public Object getItem(int i) {
        return null;
    }

    @Override
    //get the position id of the item from the list
    public long getItemId(int i) {
        return 0;
    }

    @Override

    public View getView(int position, View view, ViewGroup viewGroup) {

        // create a ViewHolder reference
        ViewHolder holder;

        //check to see if the reused view is null or not, if is not null then reuse it
        if (view == null) {
            holder = new ViewHolder();

            view = mLayoutInflater.inflate(R.layout.list_item, null);
            holder.itemName = (TextView) view.findViewById(R.id.list_item_text_view);

            // the setTag is used to store the data within this view
            view.setTag(holder);
        } else {
            // the getTag returns the viewHolder object set as a tag to the view
            holder = (ViewHolder)view.getTag();
        }

        //get the string item from the position "position" from array list to put it on the TextView
        String stringItem = mListItems.get(position);
        if (stringItem != null) {
            if (holder.itemName != null) {
                //set the item name on the TextView
                holder.itemName.setText(stringItem);
            }
        }

        //this method must return the view corresponding to the data at the specified position.
        return view;

    }

    /**
     * Static class used to avoid the calling of "findViewById" every time the getView() method is called,
     * because this can impact to your application performance when your list is too big. The class is static so it
     * cache all the things inside once it's created.
     */
    private static class ViewHolder {

        protected TextView itemName;

    }
}
  • Now, before we go to MyActivity class and implement the code we have to make some adjustments for the Search part.
  1. We have to create the xml file for the search item which will be on the list (how the row for search list will look like). So go to res-layout and create a new xml file called result_search_item.xml and copy the code below:
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:padding="5dp">

   <TextView android:layout_width="fill_parent"
             android:layout_height="30dp" android:id="@+id/search_result_text_view" android:textColor="@android:color/white"/>

</LinearLayout>
2. Now you have put some strings in the strings.xml file. So go to res-values-strings.xml and copy the following code: You have to use this strings later because it seems that the search won’t work if we use hardcoded strings.
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">ListViewSearchView</string>
    <string name="search_label">Search</string>
    <string name="search_hint">Search Names</string>
</resources>

3. Now we have to define the Searchable part. In order to do this you have to create a new xml file in res-xml. If you don’t have the xml directory follow the following steps:

    • Right click on res – New – Android Resource Directory
    • Now that we have the xml directory we have to create the new xml file. Call it searchable.xml and copy the code below: This file will be used in the manifest later.
<?xml version="1.0" encoding="utf-8"?>

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
            android:label="@string/search_label"
            android:hint="@string/search_hint"/>
  • Now go to the Manifest.xml file and copy the following code just before </activity> tag.
<intent-filter>
                <action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
                       android:resource="@xml/searchable" />

E.g. My Manifest file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example"
          android:versionCode="1"
          android:versionName="1.0">
    <uses-sdk android:minSdkVersion="15"/>
    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
        <activity android:name="com.example.MyActivity"
                  android:label="@string/app_name">

            <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>
    </application>
</manifest>
  • No let’s create the FTS3 Virtual Table for fast searches. Create a new java class and name it SearchHelper.
package com.example;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;


public class SearchHelper {
    public static final String COLUMN_NAME = "name";

    private static final String TAG = "SearchHelper";
    private DatabaseHelper mDbHelper;
    private SQLiteDatabase mDb;

    private static final String DATABASE_NAME = "Data";
    private static final String FTS_VIRTUAL_TABLE = "Info";
    private static final int DATABASE_VERSION = 1;

    //Create a FTS3 Virtual Table for fast searches
    private static final String DATABASE_CREATE =
            "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE + " USING fts3("
                    + COLUMN_NAME
                    + " UNIQUE (" + COLUMN_NAME + "));";


    private final Context context;

    private static class DatabaseHelper extends SQLiteOpenHelper {

        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }


        @Override
        public void onCreate(SQLiteDatabase db) {
            Log.w(TAG, DATABASE_CREATE);
            db.execSQL(DATABASE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
            onCreate(db);
        }
    }

    public SearchHelper(Context ctx) {
        this.context = ctx;
    }

    public SearchHelper open() throws SQLException {
        mDbHelper = new DatabaseHelper(context);
        mDb = mDbHelper.getWritableDatabase();
        return this;
    }

    public void close() {
        if (mDbHelper != null) {
            mDbHelper.close();
        }
    }


    public long createList(String name) {

        ContentValues initialValues = new ContentValues();


        initialValues.put(COLUMN_NAME, name);

        return mDb.insert(FTS_VIRTUAL_TABLE, null, initialValues);

    }


    public Cursor searchByInputText(String inputText) throws SQLException {

        String query = "SELECT docid as _id," +
                COLUMN_NAME +  " from " + FTS_VIRTUAL_TABLE +
                " where " + COLUMN_NAME + " MATCH '" + inputText + "';";

        Cursor mCursor = mDb.rawQuery(query,null);

        if (mCursor != null) {
            mCursor.moveToFirst();
        }
        return mCursor;

    }


    public boolean deleteAllNames() {

        int doneDelete = mDb.delete(FTS_VIRTUAL_TABLE, null , null);
        return doneDelete > 0;

    }
}

This class is used for creating a virtual database for faster searches. Also it keeps the queries used for search.

  • Now we can finally go to MyActivity class and copy the following code:
package com.example;

import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.*;

import java.util.ArrayList;

public class MyActivity extends Activity implements SearchView.OnQueryTextListener,
        SearchView.OnCloseListener {

    private ListView myList;
    private SearchView searchView;
    private SearchHelper mDbHelper;
    private MyCustomAdapter defaultAdapter;
    private ArrayList<String> nameList;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        nameList = new ArrayList<String>();

        //for simplicity we will add the same name for 20 times to populate the nameList view
        for (int i = 0; i < 20; i++) {
            nameList.add("Diana" + i);
        }

        //relate the listView from java to the one created in xml
        myList = (ListView) findViewById(R.id.list);

        //show the ListView on the screen
        // The adapter MyCustomAdapter is responsible for maintaining the data backing this nameList and for producing
        // a view to represent an item in that data set.
        defaultAdapter = new MyCustomAdapter(MyActivity.this, nameList);
        myList.setAdapter(defaultAdapter);

        //prepare the SearchView
        searchView = (SearchView) findViewById(R.id.search);

        //Sets the default or resting state of the search field. If true, a single search icon is shown by default and
        // expands to show the text field and other buttons when pressed. Also, if the default state is iconified, then it
        // collapses to that state when the close button is pressed. Changes to this property will take effect immediately.
        //The default value is true.
        searchView.setIconifiedByDefault(false);

        searchView.setOnQueryTextListener(this);
        searchView.setOnCloseListener(this);

        mDbHelper = new SearchHelper(this);
        mDbHelper.open();

        //Clear all names
        mDbHelper.deleteAllNames();

        // Create the list of names which will be displayed on search
        for (String name : nameList) {
            mDbHelper.createList(name);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mDbHelper  != null) {
            mDbHelper.close();
        }
    }

    @Override
    public boolean onClose() {
        myList.setAdapter(defaultAdapter);
        return false;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        displayResults(query + "*");
        return false;
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        if (!newText.isEmpty()){
            displayResults(newText + "*");
        } else {
            myList.setAdapter(defaultAdapter);
        }

        return false;
    }

    /**
     * Method used for performing the search and displaying the results. This method is called every time a letter
     * is introduced in the search field.
     *
     * @param query Query used for performing the search
     */
    private void displayResults(String query) {

        Cursor cursor = mDbHelper.searchByInputText((query != null ? query : "@@@@"));

        if (cursor != null) {

            String[] from = new String[] {SearchHelper.COLUMN_NAME};

            // Specify the view where we want the results to go
            int[] to = new int[] {R.id.search_result_text_view};

            // Create a simple cursor adapter to keep the search data
            SimpleCursorAdapter cursorAdapter = new SimpleCursorAdapter(this, R.layout.result_search_item, cursor, from, to);
            myList.setAdapter(cursorAdapter);

            // Click listener for the searched item that was selected
            myList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

                    // Get the cursor, positioned to the corresponding row in the result set
                    Cursor cursor = (Cursor) myList.getItemAtPosition(position);

                    // Get the state's capital from this row in the database.
                    String selectedName = cursor.getString(cursor.getColumnIndexOrThrow("name"));
                    Toast.makeText(MyActivity.this, selectedName, Toast.LENGTH_SHORT).show();

                    // Set the default adapter
                    myList.setAdapter(defaultAdapter);

                    // Find the position for the original list by the selected name from search
                    for (int pos = 0; pos < nameList.size(); pos++) {
                        if (nameList.get(pos).equals(selectedName)){
                            position = pos;
                            break;
                        }
                    }

                    // Create a handler. This is necessary because the adapter has just been set on the list again and
                    // the list might not be finished setting the adapter by the time we perform setSelection.
                    Handler handler = new Handler();
                    final int finalPosition = position;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {

                            myList.setSelection(finalPosition);
                        }
                    });

                    searchView.setQuery("",true);
                }
            });

        }
    }
}

And now let’s see the result!

The default list (the one with our data)

The Search List (notice that the names’ font is smaller so don’t confuse it with the default one)
The original list is scrolled to the searched name

References: http://www.mysamplecode.com/2011/11/android-searchview-using-sqlite-fts3.html

keyboard_arrow_up