Search code examples
androidandroid-fragmentsandroid-actionbarsettingspreferences

What's the proper way to set up an Android PreferenceFragment?


I'm trying to implement a basic settings activity in an Android app and either get a blank white screen or a crash. The documentation and samples I've seen aren't helping because they're either old or inconsistent. For example, depending on where you look, the settings activity should either extend Activity, PreferenceActivity, or AppCompatPreferenceActivity (part of the File>New>Activity>Settings Activity).

developer.android.com says you should implement the following:

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();
    }
}

Yet, the Settings Activity generated in Android Studio uses does not make this call for any of the three fragments it creates. It uses preference headers.

So here are my questions:

  1. If you're using a simple, single preferences.xml file with a single PreferenceFragment and pre-API 19 compatibility is not a requirement, what class should SettingsActivity extend? Activity, PreferenceActivity, or AppCompatPreferenceActivity (for all its support methods and delegation)?
  2. Do you need to call getFragmentManager().beginTransaction().replace(android.R.id.content, new SettingsFragment()).commit() in SettingsActivity.onCreate()?
  3. With various combinations, I'm either getting a blank white settings screen with no action bar or a crash. What's the right way to setup a single PreferencesFragment within an activity that displays the app action bar?

Solution

  • what class should SettingsActivity extend?

    What worked for me was extending AppCompatActivity.

    static final String ANIMATION = "animation" ;
    static final String COUNTDOWN_ON_OFF = "countdown_on_off";
    
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
    
        if (getFragmentManager().findFragmentById(android.R.id.content) == null)
        {
            getFragmentManager().beginTransaction().add(android.R.id.content, new Prefs()).commit();
        }
    }
    

    I kicked out all the generated code related to preference headers and kept some template methods/ variables (which Android Studio generated in some earlier version) for my PreferenceFragment

    public static class Prefs extends PreferenceFragment
    {
        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.preferences);
    
            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
            // to their values. When their values change, their summaries are
            // updated to reflect the new value, per the Android Design
            // guidelines.
    
            // findPreference() uses android:key like in preferences.xml !
    
            bindPreferenceSummaryToValue(findPreference(ANIMATION));
    
        }
    }
    

    A static method in my Activity class (adapted from the template). You may want to check for other preference types:

     /**
     * Binds a preference's summary to its value. More specifically, when the
     * preference's value is changed, its summary (line of text below the
     * preference title) is updated to reflect the value. The summary is also
     * immediately updated upon calling this method. The exact display format is
     * dependent on the type of preference.
     *
     * @see #sBindPreferenceSummaryToValueListener
     */
    private static void bindPreferenceSummaryToValue(Preference preference)
    {
        // Set the listener to watch for value changes.
        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
    
        // Trigger the listener immediately with the preference's
        // current value.
    
        if (preference instanceof CheckBoxPreference)
        {
            sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
                                                                     PreferenceManager
                                                                             .getDefaultSharedPreferences(preference.getContext())
                                                                            .getBoolean(preference.getKey(), true));
        }
        else
        {
            sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
                                                                     PreferenceManager
                                                                             .getDefaultSharedPreferences(preference.getContext())
                                                                             .getString(preference.getKey(), ""));
        }
    }
    

    And finally, the Preference.OnPreferenceChangeListener as static variable in the Activity (also adapted from the template):

       /**
     * A preference value change listener that updates the preference's summary
     * to reflect its new value.<br>
     * template by Android Studio minus Ringtone Preference
     */
    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener()
    {
        @Override
        public boolean onPreferenceChange(Preference preference, Object value)
        {
            String stringValue = value.toString();
    
            if (preference instanceof ListPreference)
            {
                // For list preferences, look up the correct display value in
                // the preference's 'entries' list.
                ListPreference listPreference = (ListPreference) preference;
                int index = listPreference.findIndexOfValue(stringValue);
    
                // Set the summary to reflect the new value.
                preference.setSummary(
                        index >= 0
                                ? listPreference.getEntries()[index]
                                : null);
    
            }
            else if (preference instanceof RingtonePreference)
            {
                // my app didn't need that
                return true;
            }
            else if (preference instanceof CheckBoxPreference)
            {
                Context ctx = preference.getContext();
                boolean isChecked = (Boolean) value;
    
                if (preference.getKey().equals(ANIMATION))
                {
                    preference.setSummary(isChecked ? ctx.getString(R.string.sOn) : ctx.getString(R.string.sOff));
                }
                else if (preference.getKey().equals(COUNTDOWN_ON_OFF))
                {
                    preference.setSummary(isChecked ? ctx.getString(R.string.sWhenPaused) : ctx.getString(R.string.sNever) );
                }
            }
            else
            {
                // For all other preferences, set the summary to the value's
                // simple string representation.
                preference.setSummary(stringValue);
            }
            return true;
        }
    };
    }