PreferenceChangeListener

The differences between PreferenceChangeListener and SharedPreferenceChangeListener are:

  • SharedPreferenceChangeListener is triggered after any value is saved to the SharedPreferences file.
  • PreferenceChangeListener is triggered before a value is saved to the SharedPreferences file. Because of this, it can prevent an invalid update to a preference.
  • PreferenceChangeListeners are also attached to a single preference.

So it can be used as follows:

  • User updates a preference.
  • PreferenceChangeListener triggered for that preference e.g. to validate input.
  • The new value is saved to the SharedPreference file.
  • onSharedPreferenceChanged listeners are triggered.

Implement Preference.OnPreferenceChangeListener:

public class SettingsFragment extends PreferenceFragmentCompat implements OnSharedPreferenceChangeListener, Preference.OnPreferenceChangeListener

In the override for onCreatePreferences attach the listener to the chosen Preference:

@Override
public void onCreatePreferences(Bundle bundle, String s) {
         /* Other preference setup code code */
        //...
        Preference preference = findPreference(getString(R.string.pref_xxxx_key)); // Key for the Preference to watch. Customise
        preference.setOnPreferenceChangeListener(this); // Attach the listener to the Preference
}

In this example we will use the onPreferenceChange override to validate the input and restrict it to a number between 0.1 and 3:

@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
   Toast error = Toast.makeText(getContext(), "Please select a number between 0.1 and 3", Toast.LENGTH_SHORT); // Error Toast if input invalid

   String xxxxKey = getString(R.string.pref_xxxx_key);
   if (preference.getKey().equals(xxxxKey)) {
       String stringInput = (String) newValue; // Cast the Object o a String
       try {
           float input = Float.parseFloat(stringInput); // Try converting to a float
           if (input > 3 || input <= 0) { // If outside the accepted range...
               error.show(); // ...show the error Toast...
               return false; // ...and fail to register the new change
           }
       } catch (NumberFormatException nfe) { // If cannot convert to float then throw an exception
           error.show();
           return false;
       }
   }
   return true; // Otherwise accept the value
}

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.10

EditTextPreference

Typical xml code for an edit text preference entry.

    <EditTextPreference
        android:defaultValue="@string/pref_edit_text_default" // Default value for preference
        android:key="@string/pref_edit_text_key" // Key value for preference
        android:title="@string/pref_edit_text_label" /> // Title that appears in preference field

Setting Preference Summary/Label for ListPreference

ListPreference items in Preference screens do not automatically provide a label showing their current value. You can create labels for all list Preference items as so:

In the Settings Fragment onCreatePreferences override, get the Shared Preferences and the Preference screen. Then iterate through all of the Preferences in the screen, checking whether they are tickboxes. Any that aren't will have their value stored and passed to a setPreferenceSummary function along with the Preference itself:

    @Override
    public void onCreatePreferences(Bundle bundle, String s) {

        addPreferencesFromResource(R.xml.pref_xxxxx); // Reference to xml resource
        SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); // Retrieve the Shared Preferences from the Preference screen
        PreferenceScreen preferenceScreen = getPreferenceScreen(); // Retrieve the Preference screen
        int count = preferenceScreen.getPreferenceCount(); // The number of Preferences within the screen

        for (int i = 0; i < count; i++ ) { // For each Preference...
            Preference p = preferenceScreen.getPreference(i); // ...retrieve the Preference...
            if (!(p instanceof CheckBoxPreference)) { // ...make sure it isn't a checkbox...
                String value = sharedPreferences.getString(p.getKey(), ""); // ...then retrieve its value (can't use getString on a Boolean, so another reason to exclude tickboxes)...
                setPreferenceSummary(p, value); // ...and send the Preference and its value to the setPreferenceSummary function
            }
        }
    }

The setPreferenceSummary function is as follows:

    private void setPreferenceSummary(Preference preference, String value) {
        if (preference instanceof ListPreference) { // If it is a ListPreference...
            ListPreference listPreference = (ListPreference) preference; // ...cast it to an object of type ListPreference...
            int prefIndex = listPreference.findIndexOfValue(value); // ...retrieve the index of that value...
            if (prefIndex >= 0) {
                listPreference.setSummary(listPreference.getEntries()[prefIndex]); // ...and get the corresponding label for it, setting it as the Preference Summary
            }
        } else if (preference instanceof EditTextPreference) {
            // For EditTextPreferences, set the summary to the value's simple string representation.
            preference.setSummary(value);
        }
    }

Implement an OnSharedPreferenceChangeListener to make sure changes are implemented without having to force an onCreate (e.g. device rotation). Within the required onSharedPreferenceChanged override, again check to see if the preference is not a checkbox, and if so send the preference to the setPreferenceSummary function along with its value:

public class SettingsFragment extends PreferenceFragmentCompat implements OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        Preference preference = findPreference(key);
        if (preference != null) {
            if (!(preference instanceof CheckBoxPreference)) {
                String value = sharedPreferences.getString(preference.getKey(), "");
                setPreferenceSummary(preference, value);
            }
        }
    }
}

Register and unregister the OnSharedPreferenceChangeListener with the following overrides:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.08

ListPreference

Typical xml code for list preference entry (using colours as dummy data).

    <ListPreference
        android:defaultValue="@bool/pref_list_default" // Default value from list
        android:key="@string/pref_list_key" // Key name to identify preference
        android:entries="@string/pref_list_entries" // Array with all the option labels
        android:entryValues="@string/pref_list_entry_values" // Array with all the values
        android:title="@string/pref_list_label" /> // Label for preference

In strings.xml (note that keys and values should always be 'translatable: false'):

    <string name="pref_list_label">Colour</string>
    <string name="pref_list_label_red">Red</string>
    <string name="pref_list_label_blue">Blue</string>
    <string name="pref_list_label_green">Green</string>
    <string name="pref_list_value_red" translatable="false">red</string>
    <string name="pref_list_value_blue" translatable="false">blue</string>
    <string name="pref_list_value_green" translatable="false">green</string>
    <string name="pref_list_default" translatable="false">red</string>
    <string name="pref_list_key" translatable="false">color</string>

In arrays.xml:

<resources>
    <array name="pref_list_entries">
        <item>@string/pref_color_label_red</item>
        <item>@string/pref_color_label_blue</item>
        <item>@string/pref_color_label_green</item>
    </array>
    <array name="pref_list_entry_values">
        <item>@string/pref_color_value_red</item>
        <item>@string/pref_color_value_blue</item>
        <item>@string/pref_color_value_green</item>
    </array>
</resources>

CheckBoxPreference

Typical xml code for checkbox preference entry.

    <CheckBoxPreference
        android:defaultValue="@bool/pref_show_text_default" // Default Boolean value: true (ticked) or false (unticked)
        android:key="@string/pref_show_text_key" // Key name to identify preference
        android:summaryOff="@string/pref_show_false" // Text to show if unticked
        android:summaryOn="@string/pref_show_true" // Text to show if ticked
        android:title="@string/pref_show_text_label" /> // Label for preference

Settings or Preferences – Making Changes In App

How to make user changes to options within the Settings Fragment affect changes in the app.

In the Activity call a function to setup shared preferences, then create that function. We will also link the shared preference object with the onSharedPreferenceChangeListener using .registerOnSharedPreferenceChangeListener:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_xxxxx);
        mTargetView =  findViewById(R.id.activity_xxxxx); // View to be affected by the preference change
        setupSharedPreferences(); // Call custom function to set up preferences
    }

    private void setupSharedPreferences() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Get a reference to the default shared preferences from the PreferenceManager class
        mTargetView.setShowText(sharedPreferences.getBoolean(getString(R.string.pref_xxxxx), getResources().getBoolean(R.bool.pref_xxxxx_default))); // Get the value of a preference and use it to call a function (in this case, the custom function mTargetView.setShowText) which will change behaviour in the app
        sharedPreferences.registerOnSharedPreferenceChangeListener(this); // We can use 'this' as context as the Activity will implement OnSharedPreferenceChangeListener (see next)
    }

To make sure the Activity implements the change without having to force an onCreate (e.g.rotate device) implement an OnSharedPreferenceChangeListener within the relevant Activity:

public class xxxxxActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // This function already passes us a reference to the sharedPreferences and key, so we don't need to use the PreferenceManager
        if (key.equals(getString(R.string.pref_show_bass_key))) {
            mTargetView.setShowBass(sharedPreferences.getBoolean(key, getResources().getBoolean(R.bool.pref_show_text_default)));
        }
    }
}

Unregister the OnSharedPreferenceChangeListener when the Activity is shut down using .unregisterOnSharedPreferenceChangeListener:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
    }

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.05

Settings or Preferences Fragment

How to set up a settings Activity with a PreferenceFragment.

Create an Activity named SettingsActivity (including the activity_settings.xml layout file) with the following code:

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
        ActionBar actionBar = this.getSupportActionBar();

        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true); // Enables the up button in the Action Bar
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            NavUtils.navigateUpFromSameTask(this); // Navigates up to parent activity (add supporting code to AndroidManifest.xml) OR...
            onBackPressed(); // ...replicates the function of the Back button (i.e. returns to last Activity) DO NOT ADD BOTH
        }
        return super.onOptionsItemSelected(item);
    }
}

To construct a menu with the option to enter the Settings fragment, create a new resource folder called 'menu' (if it doesn't already exist) and create 'xxxxx_menu.xml' (customise) as follows:

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">

    <item android:id="@+id/action_settings" // Rename the id as necessary
     android:title="Settings" // Text that will appear in the menu listing - move to strings.xml
     android:orderInCategory="100" // Order of item in menu
     app:showAsAction="never" /> // Determine whether it will appear in the action bar (if room) as opposed to the dropdown menu

</menu>

Add the following code to your Activity to add the menu:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.xxxxx_menu, menu); // Customise according to xml name
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(item.getItemId() == R.id.action_settings) {
            Intent startSettingsActivity = new Intent(this, SettingsActivity.class);
            startActivity(startSettingsActivity);
            return true; // Deliver explicit Intent to start Settings Activity on click
        }
        return super.onOptionsItemSelected(item);
    }

Within the AndroidManifest.xml file, make the SettingsActivity a child of the main activity (DO NOT do this if you intend to access your Settings screen from multiple Activities as this will lock it to one parent Activity. Instead, leave out lines 12-15 and use the onBackPressed() option is your Settings Activity) and also make the main activity launch mode 'singleTop' to prevent the Activity being remade on returning:

        <activity
         android:name=".xxxxxActivity" // Customise
         android:launchMode="singleTop"> // Stops parent activity being remade on exiting Settings
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity // Register
         android:name=".SettingsActivity"
         android:label="Settings" // ADD THESE LINES. Move text value to strings.xml
         android:parentActivityName=".xxxxxActivity"> // ADD THESE LINES ONLY IF NAVIGATING TO PARENT. Customise
            <meta-data // ADD THESE LINES ONLY IF NAVIGATING TO PARENT
             android:name="android.support.PARENT_ACTIVITY" // ADD THESE LINES ONLY IF NAVIGATING TO PARENT
             android:value=".xxxxxActivity" /> // ADD THESE LINES ONLY IF NAVIGATING TO PARENT. Customise
        </activity>

In the res -> xml folder create a file called pref_xxxxx.xml (customise) to define the options within the Settings Fragment:

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

    <CheckBoxPreference // In this example we have a tickbox option android:defaultValue="true" // Default value of true/ticked
     android:key="show_text" // The reference of the value which can be changed
     android:summaryOff="@string/hidden" // Text to be shown when tickbox is not checked
     android:summaryOn="@string/shown" // Text to be shown when tickbox is checked
     android:title="@string/show_text" // Title of option
     />

</PreferenceScreen>

Create a class called SettingsFragment as follows (may require the addition of: implementation 'com.android.support:preference-v7:28.0.0' to your gradle file):

public class SettingsFragment extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle bundle, String s) {
        addPreferencesFromResource(R.xml.pref_xxxxx); // Add the preferences xml to display the options (customise)
    }
}

Adjust the activity_settings.xml file to reflect a Fragment layout:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_settings"
android:name="xxx.xxxxx.xxx.xxxxx.SettingsFragment" // Customise xxxxx with app name e.g. android.com.example.app
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>

Add a theme in the styles.xml file:

        <item name="preferenceTheme">@style/PreferenceThemeOverlay</item> // App will crash without this addition

To make user changes to the Settings Fragment take effect, follow this post: Settings or Preferences - Making Changes In App

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.01 & 02