Sunday, March 16, 2008

My first meeting with the SimpleAdapter widget

Such a shame. Normally, Android developers have their SimpleAdapter adventures right at the beginning of their Android development career, meanwhile I met this beast just now. I wouldn't say that this UI feature is overdocumented and that there are truckloads of example programs out there (if there are, Google does not find them) so I decided to share my experiences with whoever cares to read this blog entry.

It all started with a very simple wish that I wanted to create a list like this:


Each list entry has two lines and the lines are styled differently. Fortunately, there is an example program at the ListActivity documentation page which happens not to work and takes the input behind the list from a database. So it took me some time to find my friend, the SimpleAdapter widget and to find out, how to use it.

You can download the example program from here.

The root of the problem is that each element of this list is a complex view, consisting of two TextViews (XML mangling because of blog engine limitations):

[?xml version="1.0" encoding="utf-8"?]
[LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"]

[TextView android:id="@+id/text1"
android:textSize="16px"
android:textStyle="bold"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/]

[TextView android:id="@+id/text2"
android:textSize="12px"
android:textStyle="italic"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/]
[/LinearLayout]

The corresponding SimpleAdapter maps an element from list of objects to this view.

SimpleAdapter notes = new SimpleAdapter(
this,
list,
R.layout.main_item_two_line_row,
new String[] { "line1","line2" },
new int[] { R.id.text1, R.id.text2 } );

Now this is not a trivial line of code. What it says is:
  • "list" is a reference to an object implementing the List interface. Each element in this list is an object implementing the Map interface. In our implementation example, the list is ArrayList and the elements in this list are HashMaps.
  • The layout describing one row in the main list is defined in layout/main_item_two_line_row.xml file.
  • In each individual HashMap, there are two key-value pairs. The keys are "line1" and "line2", the corresponding values are arbitrary objects whose toString() method yields the value displayed in the list row. In our case, the values are Strings.
  • Values stored with key "line1" will be displayed in the TextView whose id is "text1". Similarly, "line2" Map key is associated with "text2" TextView.
You can try the program following the instructions in the Start here entry. You can add items to the list using the menu.

Tuesday, March 11, 2008

Observing content

Android documentation makes the point about content providers that they are perfect tool for hiding data access details. The Android content provider API has a clever little feature, however, that allows applications (and services) to observe changes of a dataset. The use case is that some component manipulates the persistent dataset that other components depend upon. The usual pattern is that the component makes the changes then invokes some sort of notification interface so that the dependent components are aware that the dataset was changed. If such a strong coupling is not possible, the dependent components may even need to poll for changes.

The Android content provider framework allows a much more elegant design. As datasets are identified by unique URIs, it is possible to ask for notifications if a certain URI is changed. The framework is the following.

  1. Well-behaving content providers are expected to notify the content resolver if they do something that may potentially change their dataset. This includes insert and update operations but registering database cursors returned by query operations is also necessary because cursors may be used to update data.
  2. Components interested in dataset changes register at the content resolver. If any URI is manipulated that they registrated for, they get notification.
You can download the example program from here.

Click here to access the example program adapted to Android SDK 2.3.3.

The content observer framework depends on the cooperation of content providers and content observers. Content providers are expected to notify content resolver that they have updated the dataset. You can observe this in our well-known simple provider, SimpleStringDataProvider.java.

In insert method:

getContext().getContentResolver().notifyChange(uri, null);
This notifies all the observers registered for that particular URI that change happened.

In query method:

c.setNotificationUri(getContext().getContentResolver(), url);

where c is reference to a Cursor object. If the data behind the cursor's position is updated (Cursor.update*(), Cursor.commitUpdates()), the given URI will be notified.

The other side of the picture is the observer. In this simple example, the observer is located in the DPObserver activity. As the observer is unregistered using the observer object reference, the observer is registered at onStart() and unregistered at onStop() because after onStop() the state of the activity may be lost.

In registerContentObservers:

ContentResolver cr = getContentResolver();
stringsObserver = new StringsContentObserver( handler );
cr.registerContentObserver( SimpleString.Strings.CONTENT_URI, true, stringsObserver );


We requested notification for the content URI allocated to the Strings provider plus all its descendants. This is important because e.g. the Strings provider generates URIs for newly allocated items like the following: content://aexp.dpobserver.SimpleString/strings/[id] where [id] is an integer number. These URIs are not equals to the Strings provider's base URI, these are descendants of the base URI.

The observer object is a child of the android.database.ContentObserver class. Its onChange() method is called by the content resolver if there is a change of the data behind the URI the observer was registered for. Unfortunately the URI that triggered the invocation is not passed to the method. The onChange() method is called in the context of a Handler. In our case, this Handler uses the UI thread of the activity.

You can try the application by launching the DPObserver activity and monitoring the log (adb logcat from the command prompt). Whenever you add a new data item (by clicking a menu item), the onChange() will generate a log message.

Now comes the interesting bit. We actually registered another observer, that one for the URI of content://contacts/people and descendants which belongs to the Contacts application. Launch now the Contacts application and add a new user. You will see in the log that our activity got called (except if it was stopped by the application manager because it went into the background). At this point, we could access the Contacts content provider and find out, what entry was added. It would be much more easier if we had the URI of the added entry but finding the new entry is possible by looking for the entry with the highest ID. We could hook onto Contacts database manipulations with our own functionality. Isn't that interesting? ;-)