Programmatic and Layout fragments

I finished a deep dive into the new Fragments feature in Honeycomb.  It’s an interesting feature — combining the UI of a View with the lifecycle of an Activity — and it has a few interesting complexities.

I’m getting a kick out of the two senses of the word “fragment” evident in articles like this one, that talk about the release of the ACL (Android Compatibility Library).  The ACL allows Android releases as far back as Donut to use fragments.  The point seems to be that back-fitting Fragments into older releases reduces Android’s perceived version fragmentation problem…

I came across one issue, using Fragments, that qualifies as a “gottcha”.  I don’t think I’d call it a bug but it certainly can be a surprise.

Consider the canonical fragment demo: an application that presents a list view and then a fragment that shows content based on the selection in the list.  You might imagine an RSS reader or a mail application, or something like that.  You might expect to set up a screen layout that looks something like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
            android:id="@+id/list"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    <fragment android:name="net.callmeike.android.example.FragmentationBomb"
            android:id="@+id/fragment"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2" />
</LinearLayout>

That seems like a perfectly reasonable approach, based on the documentation.  It’s a static definition for a page consisting of the list and the fragment that you want visible when your applications starts up.  Presuming this layout lives in a file named “main.xml”, you’d make it visible like this:

@Override
public void onCreate(Bundle state) {
   super.onCreate(state);
   setContentView(R.layout.main);
   // ...

There’s already something to be careful about.  The fragment class, net.callmeike.android.example.FragmentationBomb, will be instantiated during that call to setContentView.  That means that the fragment cannot count on there being a selection in the list from which it can determine the content it should display.  It had better be able to handle an empty selection.

There’s another issue, though that is much more subtile.  A key feature of a fragment is that it can be put on the back stack.  This is nice because it makes multi-pane applications navigate just like their single-pane cousins.  When using an Android app on a small screened device, a user navigates forward onto a new screen — perhaps by selecting something — but then pops back to the previous screen simply by pushing the back button.

On a device with a larger screen, an app like this one should behave analogously. If the user selects the second item in the list, then the fifth, and then pushes the back button, you’d hope that the screen looks exactly as it did when they originally selected the second item.  The back button should back out actions, exactly as it does on a small screen (or in a web browser, for that matter).

Fragment transactions enable exactly that behavior:

FragmentTransaction xact = getFragmentManager().beginTransaction();
xact.replace(R.id.fragment, FragmentationBomb.newInstance(selection));
xact.addToBackStack(null);
xact.commit();

This bit of code puts the current fragment on the back stack and replaces it with a new fragment instance — presumably based on a new selection in the list view.  When the user pushes the back button, the back stack is popped and the previous list selection and the fragment that contains its contents are restored.  Perfect!

Well, almost.  An application implemented as described above will fail and crash with a fairly inscrutable message like this:

java.lang.IllegalStateException: Fragment did not create a view.
    at android.app.Activity.onCreateView(Activity.java:4095)

It turns out that fragments created in a layout and fragments created programmatically are very different animals and have very different lifecycles.  Replacing one with the other is bound to cause failures.  You can drive the bug by running the transaction code, once, so that there is a programmatically created fragment visible, and then rotating the screen from portrait to landscape.

A fragment that is created as part of a layout has its onCreateView method called when it leaves the Fragment.INITIALIZING state.  If the fragment is created programmatically its onCreateView method isn’t called until it leaves the Fragment.CREATED state.  When Activity.onCreate is called, the fragment is still in the Fragment.CREATED state: it’s onCreateView method has not been called and it has no view.  The moral appears to be: “Never mix layout and programmatic fragments”.

While the code could be changed so that the layout fragment reloads its content when the list view selection changes, that defeats the whole point of the fragment transaction: the back stack.  A better solution uses only programmatically created fragments.  Use a layout like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
   <ListView
            android:id="@+id/list"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    <FrameLayout
            android:id="@+id/fragment"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2" />
</LinearLayout>

…and create all of your fragments programmatically,  installing them in the view like this:

FragmentManager fragMgr = getFragmentManager();
FragmentTransaction xact = fragMgr.beginTransaction();
if (null == fragMgr.findFragmentByTag(FRAG_TAG)) {
    xact.add(R.id.fragment, FragmentationBomb.newInstance(selection), FRAG_TAG);
}

The fragment will be occupy the space created for it by the FrameLayout item.  The FRAG_TAG is just a unique identifier to make sure you don’t add that fragment more than once.  Once you’ve tagged the fragment in the layout, you can recover that exact fragment with the findFragmentByTag call.  If the fragment already exists, adding it again would leak fragments.  Using the tag guarantees that you will only create add the fragment once and can subsequently replace it.

It turns out that there’s a really slick way to handle multiple devices, using layouts and fragments.  I’ll describe that later.

Advertisements

AIDL and Fragments

Catherine is out of town, so it’s just me and the cat.  I’m trying to catch up on Fragments, AIDL and the Android Security model.

AIDL bumps into some serious problems when combined with design for inheritance.  I think the best idea is an abstract base class whose unmarshaller, the reference stored in the public CREATOR member, is a factory for all of the subtypes.

I suppose you could do it like JDBC did: each subclass registers, in a static initializer, with the base class.  Nah… that’s a bad idea.

Recent News

There have been some interesting bits of news, in the Android space, over the last couple days.

First of all, the long-rumored Amazon Android Appstore stumbled into existence as it appeared, disappeared and then, finally, re-appeared.  The fact that Amazon is calling it an “Appstore” and not an “App Store” is, apparently, insufficient distinction to placate Apple.  They are suing.

Microsoft is also suing.  They’re suing Barnes and Noble over its Android-based Nook.  Probably more interesting than a forum of patent trolls from Redmond, Linus Torvalds is not interested in suing.  Towards the end of this article he expresses his dis-interest in concerns that Android’s Bionic library may violate the GPL.

Saving the best for last, there’s a really smart article from Andreas Constantinou on how to harness the Android explosion.

“Master” building on Ubuntu and Mac

Yesterday I got the “master” branch of Android to build on both an Ubuntu 10.10 and on Snow Leopard.  I don’t know if I can build Froyo and earlier, on either.  Apparently it depends on Java 5.  It’s a low priority experiment, anyway.

The Ubuntu build was completely straight-forward, once I installed the 64-bit OS.  To my surprise, I was able to install all of the apps I normally use, including Skype!  Linux gets better and better, every time I look at it.  I frequently find myself repeating the ancient incantations that used to be necessary to get things working (/etc/init.d/smb restart), only to find that they’ve been replaced by perfectly civilized and functional UIs.

I still haven’t figured out how to drag windows between workspaces.

The Snow Leopard build was only slightly more complicated: it failed on a device out of space error.  The Google Doc says that 8G is sufficient.  Maybe back when the docs were written.  The full build occupies 10G of the 13G case-sensitive disk image I built for it.

Today is for studying AIDL

Building Android

The big task for this week is building Android from scratch.  The Google docs imply that it is now possible to build it on a Mac, although I’ve not had any luck so far.  Make fails with:

target Java: core (out/target/common/obj/JAVA_LIBRARIES/core_intermediates/classes)
libcore/luni/src/main/java/org/apache/harmony/luni/internal/net/www/protocol/http/HttpURLConnection.java:52: cannot find symbol
symbol  : class Msglocation: package org.apache.harmony.luni.util
import org.apache.harmony.luni.util.Msg;

The machine on which I used to do builds — an Ubuntu 10.10 install — no longer works, either.  Apparently Android will no longer compile on a 32-bit install.  Also, apparently, there’s no simple upgrade path from a 32-bit install to a 64-bit install.