Sunday, November 25, 2012

Final part of Android tutorial

Here we are going to take a look at ContentProvider, subclassing of SimpleCursorAdapter, ListView and assembling of all that into almost usable application.

ContentProvider


Most irritating concept of whole Android platform. That is some kind of grand unified interface to access all data publicly available on system. As we may expect from Internet search oriented company, there is Uri which starts with "content://" then we have “authority” and “base path”, like this:

private static final String AUTHORITY = "za.org.droidika.tutorial.SearchResultProvider";
private static final String TWEETS_BASE_PATH = "tweets";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
        + "/" + TWEETS_BASE_PATH);


That is not all, we also differentiate between operations on unit and bulk operations:

public static final int TWEETS = 100;
public static final int TWEET_ID = 110;
public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE
        + "vnd.org.droidika.tutorial/tweets";
public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE
        + "vnd.org.droidika.tutorial/tweets";


Further in the source code (available from here https://github.com/FBulovic/grumpyoldprogrammer) we combine our identifiers and load into UriMatcher and we add DB mappings to HashMap. That, with help of SQLiteOpenHelper, is enough of configuration and we can finally implement CRUD methods. I will explain bulk insert and you can find out what is going on in others on you own.

public int bulkInsert(Uri uri, ContentValues[] values) {
    final SQLiteDatabase db = dbInst.getWritableDatabase();
    final int match = sURIMatcher.match(uri);
    switch(match){
    case TWEETS:
        int numInserted= 0;
        db.beginTransaction();
        try {
            SQLiteStatement insert =
                db.compileStatement("insert into " + DbHelper.TABLE_NAME
                        + "(" + DbHelper.USER + "," + DbHelper.DATE
                        + "," + DbHelper.TEXT + ")"
                        +" values " + "(?,?,?)");
            for (ContentValues value : values){
                insert.bindString(1, value.getAsString(DbHelper.USER));
                insert.bindString(2, value.getAsString(DbHelper.DATE));
                insert.bindString(3, value.getAsString(DbHelper.TEXT));
                insert.execute();
            }
            db.setTransactionSuccessful();
            numInserted = values.length;
        } finally {
            db.endTransaction();
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return numInserted;
    default:
        throw new UnsupportedOperationException("unsupported uri: " + uri);
    }
}


We obtain writable instance of DB and we ask URIMatcher do we have right Uri, we do not want to attempt inserting wrong data or do bulk inserting of single row. Next we compile insert statement and open transaction. Inside for loop we assign parameters and execute inserts. If everything vent well we commit transaction and close it inside finally. At the end we ask ContentResolver to notify subscribers about new situation.
We do not pay any attention on resource management, we do not manage cursors, nothing. Suddenly SQLite is friendly and cooperative. That is probably main reason, beside ability to share data across application boundaries, why we should use providers. Naturally there is more, but it happens not in code but inside AndroidManifes.xml
Within application element we place this:

    android:name=".SearchResultProvider"
    android:authorities="za.org.droidika.tutorial.SearchResultProvider" />


Now ContentResolver knows how to find our provider.


Subclassing SimpleCursorAdapter



Since whole idea behind application was implementing something like autocompletion, we type and ListView content changes while we typing, we need custom data adapter. There is only one interesting method to implement:

public Cursor runQuery(CharSequence constraint) {
    String searchString = constraint.toString();
    if (searchString == null || searchString.length() == 0)
        c = contentResolver.query(SearchResultProvider.CONTENT_URI, null, null, null, null);
    else {
        c = contentResolver.query(SearchResultProvider.CONTENT_URI, null, DbHelper.TEXT + " like '%"
                + searchString + "%'", null, null);
    }
    if (c != null) {
        c.moveToFirst();
    }
    return c;
}


If we have search string we do like query and return cursor and if we do not have search string we return everything. Again we do not manage cursor, we just leave everything to Android and it behaves really friendly.

Assembling application


User interface is influenced with SearchableDictionary sample from Android SDK. We type in EditText on top of the screen our search string and data adapter and provider load result into ListView. In order to retrieve data we start “cron job” and it does Twitter search every three minutes and stores data into DB. MainActivity contains example how to create and use menu, how to check is network available and only nontrivial method there is this one:

private void buildList() {
    String[] columns = new String[] { DbHelper.DATE, DbHelper.TEXT,
            DbHelper.USER };
    int[] to = new int[] { R.id.textView2, R.id.textView4, R.id.textView6 };
    Cursor cursor = createCursor();
    final SearchableCursorAdapter dataAdapter = new SearchableCursorAdapter(this, R.layout.list_entry,
            cursor, columns, to);
    ListView listView = (ListView) findViewById(R.id.missingList);
    listView.setAdapter(dataAdapter);
    EditText textFilter = (EditText) findViewById(R.id.myFilter);
    textFilter.addTextChangedListener(new TextWatcher() {

        public void afterTextChanged(Editable s) {
        }

        public void beforeTextChanged(CharSequence s, int start, int count,
                int after) {
        }

        public void onTextChanged(CharSequence s, int start, int before,
                int count) {
            if (dataAdapter != null) {
                    dataAdapter.getFilter().filter(s.toString());
            }
        }
    });
    dataAdapter.setFilterQueryProvider(dataAdapter);
}


We do setup of ListView, create data adapter, assign data adapter and use TextWatcher to run queries against content provider. Not very complicated.
Again, visit repository https://github.com/FBulovic/grumpyoldprogrammer retrieve code and you have quite comprehensive example, written in such way that is easy to understand. If you are going to use it in production configure HttpClient properly, how is that done is described here http://grumpyoldprogrammer.blogspot.com/2012/10/is-it-safe.html

No comments:

Post a Comment