Tuesday, February 17, 2009

Android Custom Component Merge

I created my custom component that extends LinearLayout. In that post I show that the root element of the components XML is a <LinearLayout ...> element. That ended up getting in the way. When the LayoutInflater built the view in my Java PersonComposite extends LinearLayout the default behavior was that my java class now had one child, the LinearLayout that is the root of the XML. This meant that each use of <my.app.PersonComposite ...> in other view XML would never apply configuration to the Linearlayout that has any effect.

Wow. Even I don't understand that. Let's try this way. The LayoutInflater, actually created the following structure (details omitted for brevity):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout >
<LinearLayout >
<TextView .../>
<TextView .../>
</LinearLayout>
</LinearLayout>
So any attempts to use the component like this would never change the orientation of the two nested TextView Components because the configuration was applied to the outer LinearLayout:
<my.app.PersonComposite
android:id="@+id/person"
android:orientation="vertical"
/>
Solve this problem by using <merge> as the root element. And make sure you invoke LayoutInflater:

LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View inflatedView = inflater.inflate(R.layout.person_composite, this);
Now the person_composite.xml looks something like this:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<TextView .../>
<TextView .../>
</merge>
You can see how this works by getting the android source from git and looking at the source code for the LayoutInflater.inflate method. then search that same source tree for an xml that uses <merge> as the root element. You'll find more than one.

Sunday, February 8, 2009

Unit Test Your Custom Parcelable

Your Android application needs to pass some custom data between processes in an Intent. You will be calling Intent.putExtra(String, Parcelable). It would be easier to test if the Parcel class where not final with only private constructors. That keeps us from testing the methods individually. We are forced to perform only round trip testing -- write our Parcelable to a Parcel, then call the creator with the same Parcel instance.

There is one important step right in the middle of the round trip. The Parcel needs to be reset to be ready for read. Think of this just like working with java.nio.ByteBuffer. With ByteBuffer, when you are done writing, you call flip. With Parcel, when you want to read -- call setDataPosition(0). Here is a sample test.

I'll decoreate a Person object as a ParcelablePerson that implements the Parcelable interface and has the requisite public static final CREATOR. Following TDD I've only created enough of the implementation code below to make the test compile.
    public void testPersonTakesRoundTripThroughParcel() throws Exception {
Person testPerson = new Person();
ParcelablePerson testObject = new ParcelablePerson(testPerson);
Parcel parcel = Parcel.obtain();
testObject.writeToParcel(parcel, 0);
//done writing, now reset parcel for reading
parcel.setDataPosition(0);
//finish round trip
ParcelablePerson createFromParcel = ParcelablePerson.CREATOR.createFromParcel(parcel);

assertEquals(testPerson, createFromParcel.getPerson());
}
public class Person {
...
}
public class ParcelablePerson implements Parcelable {
private Person person;
public ParcelablePerson(Person person) {
this.person = person;
}

public Person getPerson() {
return person;
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
//call dest.write... methods
}

public static final Creator CREATOR = new Creator() {
@Override
public ParcelablePerson createFromParcel(Parcel source) {
return null;
}

@Override
public ParcelablePerson[] newArray(int size) {
return null;
}};
}