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

7 comments:

  1. I have a similar problem, I load a fragment with a webpage, and when clic on button of preference I can replace this fragment by the PrefrenceFragment, but this preference appear over the previos fragment, How Can I remove the previos fragment??

    ReplyDelete
    Replies
    1. I'm having the same problem. kind of irritating me that I even tried using fragments. To get this to work properly I created a new activity for each prefscreen... Might just be the Full Screen Activity theme...

      Delete
    2. Haha, nevermind, I got this to work, this article is the correct solution to Jgomes problema. Just make sure you use the same replace(R.id,...) in the onPrefTreeClick overide as you do in the replace(R.id,...) in the activity. Thanks Rob

      Delete
  2. could you please supply the code of actually launching the preference screen from main. I think I am missing something.. specifically how the PreferenceFragment is contained. I tried using it as an Intent to my PreferenceFragment class, but PreferenceFragement isnt an activity so that doesnt work. Do I nest the Frag in some activity (is this the FragmentView?) and then explicity start an intent to that class?

    ReplyDelete
    Replies
    1. You probably figured this out by now, but for others the code to publish the prefFrag from Main Activity is:

      public class SettingsActivity extends Activity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      // Display the fragment as the main content.
      getFragmentManager().beginTransaction()
      .replace(android.R.id.content, new SettingsFragment())
      .commit();
      }
      }

      from http://developer.android.com/guide/topics/ui/settings.html#Fragment

      Delete
  3. All you should need to do is swap your preferencefragment into the framelayout that you defined for your screen. It's no different than swapping in any other fragment actually.

    So basically you will need to have a portion of your screen set in your layout as a FrameLayout and you can put any fragment inside that FrameLayout you want.

    You should read this page on Fragments. It will help sort you out.
    http://developer.android.com/guide/topics/fundamentals/fragments.html

    ReplyDelete