While hacking on our FOOD! app for VHack Android I ran in to an issue.

FOOD! shows a couple of ListViews through ListFragment that have a header (the title) set on them. That all works pretty cool, but if I tried to add multiple ListFragment on the back stack it would crash on pressing the back key, with this exception:

java.lang.IllegalStateException: Cannot add header view to list — setAdapter has already been called.

The code for my ListFragment looks something like this:

package com.neenbedankt.listfragmentbug;
import android.annotation.TargetApi;
import android.app.ListFragment;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class TestListFragment extends ListFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TextView tv = new TextView(getActivity());
tv.setText("Hello");
getListView().addHeaderView(tv);
setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1,
new String[] { "Hello world" }));
}
}

The code crashes, because when you press back, the view is recreated through onCreateView and aparently the list is in some state that I’m not allowed to add the header.

So the big question is: WHY? First thing that popped to mind is that the ListView itself was recycled and somehow retained. This is not the case: if you debug ListFragment you’ll see that the ListView itself is null and the view is inflated.

The issue is that ListFragment will set the adapter on the ListView as part of it’s private ensureList() method that get’s called in onViewCreated. In most scenarios you would set up your adapter after creating the view add add it using the setListAdapter() method in the ListFragment (note: not on the ListView itself!). ListFragment keeps the adapter in a private member so that you could create the adapter before the list itself is created and everything would still work.

Now when you replace the fragment, the Fragment view is destroyed, but your ListFragment is still in memory, on the back stack. When you press back, that instance is used again and it’s life cycle methods like onViewCreated get called again. Then, as part of the ensureList() call, the adapter get’s set on the ListView (which is non-null now) and if you attempt to add a header view at that point…BOOM.

Fortunately there is a workaround for this: clearing the adapter when the view is destroyed, so that there won’t be an adapter to set when the view is recreated.

package com.neenbedankt.listfragmentbug;
import android.annotation.TargetApi;
import android.app.ListFragment;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class TestListFragment extends ListFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TextView tv = new TextView(getActivity());
tv.setText("Hello");
getListView().addHeaderView(tv);
setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1,
new String[] { "Hello world" }));
}
@Override
public void onDestroyView() {
super.onDestroyView();
setListAdapter(null);
}
}

So one question remains: bug or feature? Keeping the adapter can be a good thing, since it will make the list show stuff when hitting back, no need to load or wait for the loading. You could even test if the adapter is non-null and skip loading all together. On the other hand, the fragment initialization is now different depending on if it’s newly created or if it is an existing instance coming back to life, which is bad thing. The root cause is that you can’t add a header view after the adapter is set, because ListView wraps the adapter you set in a special adapter for showing the headers, so you could argue that ListView is broken in that sense. I personally think it’s a bug. If I wanted to optimize and keep the adapter, then it would make sense to keep the adapter in a member variable that I control. Now it is done “under the covers” and you have to know the implementation details to fix the issue.

In any case, I hope this write up will help others running into the same issue, happy coding!