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.

About these ads

About bmeike
Android developer and evangelist in Oakland, CA

7 Responses to Programmatic and Layout fragments

  1. Ron Thijssen says:

    Thanks, this really made my day. It is strange behaviour and I really think they should do something about it.

  2. bjdodson says:

    I spent a few days fighting with the issues of mixing programmatic and layout-based Fragments, thanks so much for this write-up!

  3. dinocore1 says:

    Thanks, this really helped. Especially the part at the end where you suggest to use fragment tags before add/replacing fragments. My project was leaking fragments and this helped clear it up.

  4. René says:

    Thanks a lot. I read some other posts regarding converting Android 2 list/details activities into fragments, but this was the final piece to complete the puzzle. I will go and study your 2 part series now.

  5. t.lorenz says:

    Perfect thanks… For me this issue was happening intermittently. I was so tired of seeing ‘MainMenuFragment did not create a view’ error reports on the market!

  6. Naty says:

    Thanks you very much. You help me a lot.

  7. Ed Anderson says:

    Your “gotchas” were especially informative – explains some of the difficulty I’ve had getting fragments and containing activity to working together.

    Ed

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: