Sunday, April 17, 2011

Honeycomb Tip #1: PreferenceFragment

One of the biggest new changes in the Android SDK is the addition of the Fragment API. The idea is that you can define multiple components of your application as Fragments and then link them together within your Activity. Building your Activity with Fragments allows you to layout your Activity's components in different ways depending on your screen size and orientation..

Ugly PreferenceScreen with default grey on black?
Today I want to talk specifically about PreferenceFragment. While porting one of my apps to Honeycomb, I discovered some interesting things about the way preferences can be displayed. If you have a lot of preferences to display, you can define preference headers which allows you to categorize your preferences logically under, well, headers. This is really nice for larger apps with a lot of preferences, but what if you only have a few preferences but still want to break some of them out into their own PreferenceScreens? If you're like me and you're porting your apps to Honeycomb, you might have just created a PreferenceFragment and tried to use your existing preference.xml layout. This will work for the top level of your preferences and they will look as expected inside your Fragment view container. However, if you select one of your nested PreferenceScreens, you see that it displays it outside of your Fragment view container and frankly looks really really bad. Not to mention if you hit the back key it won't take you back to the top level of preferences, instead it will back your Activity out to it's last state (or back to the home screen). Eeek!

Lets start with containing those nested PreferenceScreens within your defined Fragment view container. First you will need to create separate xml files for each PreferenceScreen. Like this..

prefs.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen  xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:key="nzb_server_choice" 
android:title="@string/prefs_server_choice"
android:summary="@string/prefs_server_choice_sum"
android:entries="@array/nzb_server_choice"
android:entryValues="@array/nzb_server_choice"
android:defaultValue="@string/sv_name_hellanzb" />
<PreferenceScreen
android:title="@string/prefs_hella_settings"
android:key="@string/hella_prefs_key"
android:fragment="com.brockoli.android.nzbmobilepro.HomeActivity$HellaPrefsFragment">
</PreferenceScreen>
</PreferenceScreen>

hella_prefs.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
  android:title="@string/prefs_hella_settings">
<EditTextPreference
  android:key="hella_server_url"
  android:title="@string/prefs_sv_url_title"
  android:summary="@string/prefs_hella_server_sum"
  android:defaultValue="@string/prefs_hella_server_default"/>
<EditTextPreference
  android:key="hella_server_port"
  android:title="@string/prefs_sv_port_title"
  android:summary="@string/prefs_hella_server_port_sum"
  android:defaultValue="@string/prefs_hella_server_port_default"/>
<EditTextPreference
  android:key="hella_server_password"
  android:title="@string/prefs_hella_server_pw_title"
  android:summary="@string/prefs_hella_server_pw"
  android:password="true"/>
</PreferenceScreen>

Take note of how I defined my PreferenceScreen for hella settings in prefs.xml. You need to specify android:key as well as android:fragment. This way you can key in on the specific PreferenceScreen the user selects from your code and handle it as a Fragment. Now for the code...
public static class PrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.prefs);
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen prefScreen, Preference pref) {
super.onPreferenceTreeClick(prefScreen, pref);
if (pref.getKey().equals(getActivity().getResources().getString(R.string.hella_prefs_key))) {
// Display the fragment as the main content.
getFragmentManager().beginTransaction().replace(R.id.details, new HellaPrefsFragment())
.addToBackStack(null).commit();
return true;
         }
         return false;
     }
}
public static class HellaPrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.hella_prefs);
}
}
Top level PreferenceScreen
In your code, you will need to define separate PreferenceFragment's for each PreferenceScreen you have nested. The trick to having the nested PreferenceScreen display within the Fragment view container that you specified is to catch when the user selects each PreferenceScreen. You can do this by overriding onPreferenceTreeClick callback to the parent PreferenceFragment. In that method, simply check the passed in preference's key (which you set in it's xml file) and replace the Fragment with the selected PreferenceFragment. Now you can contain all your PreferenceScreens within the same Fragment view container.


Finally, you should handle the back button correctly so the user can navigate back up through your preferences. To do this, simple add the parent PreferenceFragment to the fragment backstack. To do that you simple call the addToBackStack method of the transaction after replacing the parent fragment.

Hope this helps to clean up preferences when porting your apps to Honeycomb!

Second level PreferenceScreen within the Fragment view container