Conditional Preference Headers

Android 3.0 introduced the new, two-tier form of PreferenceActivity. You supply header+fragment pairs. Android shows the user the list of headers, and upon clicking on a header, Android shows the fragment. However, on phone-sized devices, it will do those one a time: the list of fragments fills the screen, and then the chosen fragment fills the screen. On larger devices, PreferenceActivity shows them both side by side. This is a classic rendition of the master-detail pattern.

However, it does tend to steer developers in the direction of displaying headers all of the time. For many apps, that is rather pointless, because there are too few preferences to collect to warrant having more than one header.

One alternative approach is to use the headers on larger devices, but skip them on smaller devices. That way, the user does not have to tap past a single-item ListFragment just to get to the actual preferences to adjust.

This is a wee bit tricky to implement.

The basic plan is to have smarts in onBuildHeaders() to handle this. onBuildHeaders() is the callback that Android invokes on our PreferenceActivity to let us define the headers to use in the master-detail pattern. If we want to have headers, we would supply them here; if we want to skip the headers, we would instead fall back to the classic (and, admittedly, deprecated) addPreferencesFromResource() method to load up some preference XML.

There is an isMultiPane() method on PreferenceActivity, starting with API Level 11, that will tell you if the activity will render with two fragments (master+detail) or not. In principle, this would be ideal to use. Unfortunately, it does not seem to be designed to be called from onBuildHeaders(). Similarly, addPreferencesFromResource() does not seem to be callable from onBuildHeaders(). Both are due to timing: onBuildHeaders() is called in the middle of the PreferenceActivity onCreate() processing.

So, we have to do some fancy footwork.

By examining the source code to PreferenceActivity, you will see that the logic that drives the single-pane vs. dual-pane UI decision boils down to:

onIsHidingHeaders() || !onIsMultiPane()

If that expression returns true, we are in single-pane mode; otherwise, we are in dual-pane mode. onIsHidingHeaders() will normally return false, while onIsMultiPane() will return either true or false based upon screen size. Specifically, onIsMultiPane() looks at an internal boolean resource (com.android.internal.R.bool.preferences_prefer_dual_pane), which has different definitions based upon screen size. At present, it will be true for -sw720dp devices, false otherwise.

So, we can leverage this information in a PreferenceActivity to conditionally load our headers:

public class EditPreferences extends SherlockPreferenceActivity {
  private boolean needResource=false;

  @SuppressWarnings("deprecation")
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (needResource
        || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
      addPreferencesFromResource(R.xml.preferences);
    }
  }

  @Override
  public void onBuildHeaders(List<Header> target) {
    if (onIsHidingHeaders() || !onIsMultiPane()) {
      needResource=true;
    }
    else {
      loadHeadersFromResource(R.xml.preference_headers, target);
    }
  }
}

Here, if we are in dual-pane mode, onBuildHeaders() populates the headers as normal. If, though, we are in single-pane mode, we skip that step and make note that we need to do some more work in onCreate().

Then, in onCreate(), if we did not load our headers, or if we are on API Level 10 or below, we use the classic addPreferencesFromResource() method.

The net result is that on Android 3.0+ tablets, we get the dual-pane, master-detail look with our one header, but on smaller devices (regardless of version), we roll straight to the preferences themselves.