Saving User Settings with Android Preferences (PreferenceActivity, PreferenceFragment, Headers)

An Android application will often need to have a dedicated section (page/Activity) where the user should be able to modify the app settings like enable/disable notifications, backup and sync data with cloud, etc. or changing preferences that’ll make the app behave differently because of various contexts you set like your name, profile pic, gender, birth date, etc. This also allows the app to dynamically generate the UI with the contextual details as well as use them for other purposes like “show me offers between $10-$100 only” or “I want to see people aged 15-25”. To achieve this, either you can definitely build your own set of Activities and layouts and manage a lot of the backend operations like CRUD all by yourself or use the Preference API to quickly build an interface that has the same look and feel as the System Settings app where you go to change all your phone settings.

preferenceactivity preferencefragment

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

The Android documentation has a Settings guide that covers everything about the Preference API in super detail so you should definitely study that. In this article I’ll cover the important parts involved in building a Preferences section. Just a heads up, I’ll be making use of String literals heavily in the XML resources, but you should consider using String resources because of its advantages.

Activity with Preference Fragments

It is recommended to use PreferenceFragment to display your list of options rather than using PreferenceActivity (we’ll cover than in a bit). Fragments are lot more flexible in terms of both UI and reusability. Let’s see an example where we have an Activity with an inner class that extends PreferenceFragment:

public class UserSettingsActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_user_settings);

        android.support.v7.app.ActionBar actionBar = getSupportActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);

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

    public static class SettingsFragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            addPreferencesFromResource(R.xml.user_settings);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_user_settings, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

Notice how we’ve an inner class extending PreferenceFragment that uses addPreferencesFromResource(R.xml.user_settings); to load the Preferences XML resource that does the rendering of the Preferences objects in a list on the screen. Also not you don’t use setContentView() from the Activity’s onCreate(). We also extend our Activity from ActionBarActivity. Had we subclassed PreferenceActivity then we wouldn’t have been able to use ActionBarActivity. This is how your Activity would look when working with PreferenceActivity:

public class UserSettingsActivity extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}

But since Android 3.0 (API level 11) it is highly recommended to use PreferenceFragment which is why the call to addPreferencesFromResource() from within the Activity’s onCreate() method gives a deprecated warning in Android Studio.

Defining Preference Objects in XML

So now that we’ve our Activity and Fragment classes in place, we need to take care of our Views. When using the Preference API we don’t have to write a layout file and shove in View objects into it with a ListView or something. All we have to do is create an XML resource file in which we define our Preference objects which are basically direct or indirect subclasses of the Preference class. To do this, create an XML resource in res/xml/ with whatever name you want although preferences.xml is used conventionally. We’ll name it user_settings.xml.

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <PreferenceCategory
        android:title="My Account">

        <Preference
            android:title="Username"
            android:summary="rishabhp"
            android:key="username" />

        <EditTextPreference
            android:title="Email"
            android:summary="[email protected]"
            android:key="email" />

    </PreferenceCategory>

    <PreferenceCategory
        android:title="Advanced">

        <CheckBoxPreference
            android:title="Receive Notifications"
            android:summary="Want to receive Push Notifications ?"
            android:key="receiveNotifications"
            android:defaultValue="true" />

        <ListPreference
            android:title="Country"
            android:key="country"
            android:entries="@array/country"
            android:entryValues="@array/countryValues" />

    </PreferenceCategory>

    <PreferenceCategory
        android:title="Sub Screens">

        <PreferenceScreen
            android:key="voicemail_category"
            android:title="Voicemail"
            android:persistent="false">

            <Preference
                android:title="Provider"
                android:summary="Airtel"
                android:key="provider" />

            <!-- and other preference objects ... -->

        </PreferenceScreen>

    </PreferenceCategory>

</PreferenceScreen>

As you can see the root node must be a <PreferenceScreen> element contain all the other Preference objects. The most common preferences are:

  • EditTextPreference – Opens up a dialog with an EditText whose value is stored as a String.
  • CheckBoxPreference – Shows a checkbox whose value is saved as a boolean (true if checked).
  • ListPreference – Opens a dialog with a list of radio buttons.

Each setting item in the list is a Preference object. So where are the value saved exactly ? The value is saved as key-value pairs in a default SharedPreferences file. So basically any changes you make goes into SharedPreferences as a boolean, float, int, Long, String or String set. They key is used from the android:key attribute’s value we specify for each Preference object.

You’ll notice I used a plain Preference object like this:

<Preference
    android:title="Username"
    android:summary="rishabhp"
    android:key="username" />

This is just to show some plain information that cannot be modified. If you test the code you’ll notice that <PreferenceCategory> lets you break you big list into categories (with a differentiating title) for easier consumption by the user. Nested <PreferenceScreen> is used to achieve the same goal but it opens in a new screen altogether. Your nested sub screens might not inherit the theme or show up the action bar (this depends upon the theme). It’s supposed to be a bug. Here are some threads that might help you get to know more about and solve this problem:

The proposed solutions are hacks to get around the bugs, like creating new Activities with Intents for every sub screen. You can use implicit or explicit intents to open external Activities when a particular Preference object is clicked. More on intents here.

You should find all the supported Preference objects here, just go through the direct and indirect subclasses.

Initializing SharedPreferences with Default Value

When the user loads your app for the first time, you might want to initialize the SharedPreferences file with the android:defaultValue values from your preferences XML file so that you can execute operations properly that rely on those values. In order to do that, you can execute this piece of code from your app’s main activity or the application file:

PreferenceManager.setDefaultValues(this, R.xml.user_settings, false);

This will make our app properly initialized with default settings when first loaded. The arguments are:

  • this – Application [Context].
  • R.xml.user_settings – Resource ID of the preference XML file.
  • false – Defines whether the default values should be set more than once. If true you’ll override any previous values with the defaults everytime you call this. If false the default values are set only if the method has never been called in the past (or the PreferenceManager.KEY_HAS_SET_DEFAULT_VALUES in the default shared preferences file is false).

Preference Headers

If your settings list has a bunch of options where most of them will lead to a subscreen, in that case there’s a new “headers” feature since Android 3.0 that you can use. You’ll find a lot of information about it here. The greatest benefit of using it is that it’ll automatically break your settings screen into a two-pane layout on larger screens (like tablets).

In order to use it, you basically create a headers file called say res/xml/preference_headers.xml where you list all the headers (imagine each settings item from the list) that’ll link to a particular PreferenceFragment implementation of yours. So two different headers should have two different inner PreferenceFragment classes inside your Activity which should extend PreferenceActivity. Although two headers can also use the same PreferenceFragement based on the Intent Data passed. Finally you implement the onBuildHeaders() callback in your Activity where you load the headers resource with this method call:

public class UserSettingsActivity extends PreferenceActivity {
    @Override
    public void onBuildHeaders(List<Header> target) {
        loadHeadersFromResource(R.xml.preference_headers, target);
    }
}

You should definitely go through the android guide on this to understand how you can pass data through Intents and how exactly to specify the headers in the XML resource file.

Reading Preferences and Listening for Preference Changes

We’re done with comprehending how to build the Preferences screen and how it saves any changes to SharedPreferences. Now it’s time to learn how to read the preferences so that we can actually make use of them to make the app more suitable for its users. From anywhere within our application we can get access to the SharedPreferences instance pointing to the default file by calling the static method PreferenceManager.getDefaultSharedPreferences(). You can further use methods like getString(), getInt(), etc. to fetch your saved values.

SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
String username = sharedPref.getString("username", "default value");

Now you’ll want to observe changes in the SharedPreferences through the interaction with your Preference objects so that you can either sync them with the server (cloud) or maybe update the UI (like the summary section of the object). To do that you can hook onto the changes by implementing the SharedPreferences.OnSharedPreferenceChangeListener interface in your PreferenceActivity or PreferenceFragment. Let’s see an example:

public static class SettingsFragment extends PreferenceFragment
                                    implements SharedPreferences.OnSharedPreferenceChangeListener {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.user_settings);
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (key.equals("username")) {
            Preference pref = findPreference(key);
            pref.setSummary(sharedPreferences.getString(key, ""));
        }
    }
}

The onSharedPreferenceChange() method will be called every time there’s a change in the SharedPreferences and you can check if the key matches one of your keys from your XML resource. In this case if the key is username then we just update the summary in the UI which is a good design (UX) practise. It’s even more relevant in case of ListPreference so that the user knows the current without having to tap and open up the dialog to see which radio button is checked indicating his current setting.

Now you won’t automatically start listening to changes, you’ll have to register your listener using registerOnSharedPreferenceChangeListener(). For proper lifecycle management, it is recommended to register and unregister in the onResume() and onPause() methods:

@Override
protected void onResume() {
    super.onResume();
    getPreferenceScreen()
            .getSharedPreferences()
            .registerOnSharedPreferenceChangeListener(this);
}

@Override
protected void onPause() {
    super.onPause();
    getPreferenceScreen()
            .getSharedPreferences()
            .unregisterOnSharedPreferenceChangeListener(this);
}

As pointed out in the documentation (as well as the settings guide), when registerOnSharedPreferenceChangeListener() is called, the preference manager does not store a strong reference to the listener, hence it is prone to garbage collection. Which is why you shouldn’t do this:

// Bad! The listener is subject to garbage collection!
prefs.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
        // listener implementation
    }
});

but hold a strong reference by storing it into an instance member of an object (the Activity or Fragment itself) that will anyway exist as long as the listener is needed:

SharedPreferences.OnSharedPreferenceChangeListener mListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
        // listener implementation
    }
};

// ... and then in onResume()

prefs.registerOnSharedPreferenceChangeListener(mListener);

Although I think the use case in which we made our PreferenceActivity or PreferenceFragment implementation implement SharedPreferences.OnSharedPreferenceChangeListener is sort of safe as when the Activity/Fragment is showing up on the screen its object cannot be GC’ed hence a weak reference from the preference manager won’t really matter.

Reading Preferences and Setting Summary on Load

Now when the preference screen loads for the first time you’ll have to set some of the summaries for EditTextPreference and ListPreference for instance, so that the users know their set values. Now you could either do it one by one based on the keys and call setSummary() on the Preference objects or use some sort of logic that iterates over all the items (objects) and does the job. Let’s see a simple example of that:

public static class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.user_settings);

        // show the current value in the settings screen
        for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
            pickPreferenceObject(getPreferenceScreen().getPreference(i));
        }
    }

    private void pickPreferenceObject(Preference p) {
        if (p instanceof PreferenceCategory) {
            PreferenceCategory cat = (PreferenceCategory) p;
            for (int i = 0; i < cat.getPreferenceCount(); i++) {
                pickPreferenceObject(cat.getPreference(i));
            }
        } else {
            initSummary(p);
        }
    }

    private void initSummary(Preference p) {

        if (p instanceof EditTextPreference) {
            EditTextPreference editTextPref = (EditTextPreference) p;
            p.setSummary(editTextPref.getText());
        }

        // More logic for ListPreference, etc...
    }
}

That should be helpful. It’s an exercise for you to add more if conditions to initSummary to handle ListPreference and any other that you can think of (or use). You don’t have to worry about CheckBoxPreference as the check/uncheck will automatically be handled by the preferences framework.

Building a Custom Preference

Android supports several Preference objects with a variety of UI widgets like checkbox, radio buttons (list), edit texts, etc. but there are some types for which there is not built-in solution like a number picker or a date picker. In such cases we can code our Custom Preference classes. To do this one could extend the Preference class, but then its onClick() method will need to be implemented to handle what happens when the Preference object is clicked from the list of settings and also handle other things like saving to SharedPreferences. This is why most custom settings should just extend DialogPreference that shows up a dialog and handles a lot of things out of the box.

Let’s see a custom heavily-commented Preference class that can be used to add a NumberPicker control to our settings/preferences.

public class NumberPickerPreference extends DialogPreference {

    private static String TAG = NumberPickerPreference.class.getSimpleName();

    private final int DEFAULT_VALUE = 0;

    Integer mValue;
    NumberPicker mNumberPicker;

    /*
    * We declare the layout resource file as well as the
    * text for the positive and negative dialog buttons.
    *
    * If required, instead of using `setDialogLayoutResource()`
    * to specify the layout, you can override `onCreateDialogView()`
    * and generate the View to display in the dialog right there.
    * */
    public NumberPickerPreference(Context context, AttributeSet attrs) {
        super(context, attrs);

        setDialogLayoutResource(R.layout.numberpicker_dialog);
        setPositiveButtonText("OK");
        setNegativeButtonText("Cancel");
    }

    /*
    * Bind data to our content views
    * */
    @Override
    protected void onBindDialogView(View view) {
        super.onBindDialogView(view);

        // Set min and max values to our NumberPicker
        mNumberPicker = (NumberPicker) view.findViewById(R.id.numberPicker);
        mNumberPicker.setMinValue(0);
        mNumberPicker.setMaxValue(100);

        // Set default/current/selected value if set
        if (mValue != null) mNumberPicker.setValue(mValue);
    }

    /*
    * Called when the dialog is closed.
    * If the positive button was clicked then persist
    * the data (save in SharedPreferences by calling `persistInt()`)
    * */
    @Override
    protected void onDialogClosed(boolean positiveResult) {
        if (positiveResult) {
            mValue = mNumberPicker.getValue();
            persistInt(mValue);
        }
    }

    /*
    * Set initial value of the preference. Called when
    * the preference object is added to the screen.
    *
    * If `restorePersistedValue` is true, the Preference
    * value should be restored from the SharedPreferences
    * else the Preference value should be set to defaultValue
    * passed and it should also be persisted (saved).
    *
    * `restorePersistedValue` will generally be false when
    * you've specified `android:defaultValue` that calls
    * `onGetDefaultValue()` (check below) and that in turn
    * returns a value which is passed as the `defaultValue`
    * to `onSetInitialValue()`.
    * */
    @Override
    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
        // Log.d(TAG, "boolean: " + restorePersistedValue + " object: " + defaultValue);
        if (restorePersistedValue) {
            mValue = getPersistedInt(DEFAULT_VALUE);
        }
        else {
            mValue = (int) defaultValue;
            persistInt(mValue);
        }
    }

    /*
    * Called when you set `android:defaultValue`
    *
    * Just incase the value is undefined, you can return
    * DEFAULT_VALUE so that it gets passed to `onSetInitialValue()`
    * that gets saved in SharedPreferences.
    * */
    @Override
    protected Object onGetDefaultValue(TypedArray a, int index) {
        // Log.d(TAG, "Index: " + index + " Value: " + a.getInteger(index, DEFAULT_VALUE));
        //return super.onGetDefaultValue(a, index);
        return a.getInteger(index, DEFAULT_VALUE);
    }
}

Once you’ve read and understood the comments, you’ll realize that the important steps to implement a custom Preference class/object are:

  • Override the constructors. Now in this constructor you can specify the user interface of the dialog, i.e., the Views you want to shove into it by specifying a layout resource as well as set the string values for its positive and negative buttons. If you don’t want to create the dialog contents in the constructor by defining the layout file, then you can override onCreateDialogView() to do that.
  • Override onBindDialogView() to populate the view contents with the preference data, i.e., bind the preference data so that for instance a NumberPicker shows the correct current/default/store value.
  • Override onDialogClosed() to save the setting’s value.
  • Override onGetDefaultValue() to handle your default if the preference is not yet set.
  • Override onSetInitialValue() to initialize the Preference with the stored Preference value or use the default (as well as store it).

It is important to note that each Preference can save (deal with) only one data type, i.e., only one persist*() method appropriate for the data type used by the custom Preference.

This is how the layout file (res/layout/numberpicker_dialog.xml) will look like:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <NumberPicker
        android:id="@+id/numberPicker"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

Wrapping Up

So we covered a lot about the Preference API and at the same time you should consider going through the Android settings guide too. When you want to build a Preferences or App Settings section in your Android application which is fairly plain and simple, you should consider using these APIs rather than building your own custom lists and screen to let the user adjust the app functionality according to their requirements and wants.

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

8 thoughts on “Saving User Settings with Android Preferences (PreferenceActivity, PreferenceFragment, Headers)”

  1. Hye!
    I read your whole tutorial but the thing is i want to implement a push notification which gets open in an activity and when user closes his/her app than afterwards when he opens the same app the old notification should be there. Basically in my app notification are getting received by the app but i want to store those notifications in the app.

    Please help and Thanks.

  2. what is the licence of the source code give on this page?
    Can you please release it under some licence ex: Apache Licence Version 2.0?
    (im using the code under GPLv3 or later project)
    I have sent you mail earlier regarding this topic but no reply.

  3. Hi,
    I used this code but menu_user_settings in the following part is not defined where it comes from?
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_user_settings, menu);
    return true;
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

*