9

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?
rmtheis
  • 5,992
  • 12
  • 61
  • 78
Phil
  • 1,030
  • 1
  • 16
  • 22

4 Answers4

5

Let's say we want to have a settings screen with one checkbox preference fragment as shown below:

enter image description here

Here is a step by step guide on how to build a settings activity where you can add some preferences to toggle or change the configurations for your Android app:

  1. Add a dependency for support of preference fragment in build.gradle file for app module:

    dependencies {
        compile 'com.android.support:preference-v7:25.1.0'
    }
    
  2. Add xml Android resource directory inside res directory.

  3. Inside xml directory, add a new XML resource file named pref_visualizer.xml as below. We're going to add one check-box preference fragment inside it.

    <?xml version="1.0" encoding="utf-8"?>
    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="show_base"
            android:summaryOff="Bass will not be shown currently."
            android:summaryOn="Bass will be shown currently."
            android:title="Show Bass"
            />
    </PreferenceScreen>
    

    PreferenceScreen is the root tag which can hold as many preference fragments as you want. If you want to add more configurations of type list or text box then you need to add it here as a child of PreferenceScreen tag.

  4. Add a new Java class named SettingsFragment which will host PreferenceScreen. It should extend PreferenceFragmentCompat class as shown below:

    import android.content.SharedPreferences;
    import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
    import android.os.Bundle;
    import android.support.v7.preference.CheckBoxPreference;
    import android.support.v7.preference.EditTextPreference;
    import android.support.v7.preference.ListPreference;
    import android.support.v7.preference.Preference;
    import android.support.v7.preference.PreferenceFragmentCompat;
    import android.support.v7.preference.PreferenceScreen;
    import android.widget.Toast;
    
    
    public class SettingsFragment extends PreferenceFragmentCompat {
    
    
    @Override
    public void onCreatePreferences(Bundle bundle, String s) {
            addPreferencesFromResource(R.xml.pref_visualizer);
        }
    }
    
  5. Now comes the final part where we build the association between an activity in the app and SettingsFragment class which hosts PreferenceScreen. Add a new activity named SettingsActivity which inherits from AppCompatActivity class. SettingsActivity class will act as the container for PreferenceScreen.

Java file for SettingsActivity:

import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MenuItem;

public class SettingsActivity extends AppCompatActivity {

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

}

Layout file for SettingsActivity is shown below (activity_settings.xml). Here android.name property is the crux. It connects this activity to any of the classes present in your entire project which are inheriting from PreferenceFragmentCompat class. I had only one such class named SettingsFragment. You might have more than one class inheriting from PreferenceFragmentCompat class if you app has multiple settings screen.

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

You're all set!

RBT
  • 24,161
  • 21
  • 159
  • 240
  • Looking up on https://developer.android.com/reference/android/preference/PreferenceScreen, the class can be imported via "android.preference.PreferenceScreen". Why do you choose another name "android.support.v7.preference.PreferenceScreen"? – Kolodez Dec 19 '20 at 08:45
4

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;
    }
};
}
Bö macht Blau
  • 12,820
  • 5
  • 40
  • 61
  • This works--except the action bar is missing. In SettingsActivity.onCreate(), a call to getSupportActionBar() returns null even though the activity is set as a child of MainActivity, which has an action bar, in AndroidManifest.xml. I also call setHasOptionsMenu() in SettingsFragment.onCreate() but that's probably moot. How are you able to display the ActionBar? – Phil Jul 13 '17 at 00:13
  • 1
    @Phil - I used an Activity type which has an ActionBar by default. If you have to set your ActionBar then you should copy that part from your MainActivity and put the Fragment not in R.id.content but in the container below the Toolbar just like some other Fragment – Bö macht Blau Jul 13 '17 at 04:00
  • Thanks @0X0nosugar. The reason I ask such a basic sounding question is I'm using the SettingsActivity inserted by Android Studio and it doesn't need to inflate a Toolbar nor does it even define an XML fragment nor specify its position. It just calls getSupportActionBar() and it gets it. My SettingsActivity, based on it, calls getSupportActionBar() and gets null. Either I've left out a piece of critical logic somewhere or the heading-style preferences includes some implicit ActionBar handling. – Phil Jul 14 '17 at 01:43
  • 1
    Okay, I found what was blocking the action bar. My mistake. I had android:theme="@style/AppTheme.NoActionBar" in the wrong place. When I put it in the correct tag, , along with android:theme=@style/AppTheme" in , the action bar appeared. Thanks again. – Phil Jul 14 '17 at 05:00
  • 1
    @Phil - You're welcome :) Sorry I didn't find the time to reply earlier, but my guess also would have been that having an ActionBar or not depends on setting the Activity style. Happy coding! – Bö macht Blau Jul 14 '17 at 16:33
2

Here's on Kotlin and android-x :

gradle:

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.core:core-ktx:1.2.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
implementation 'com.google.android.material:material:1.1.0-alpha08'
implementation "androidx.preference:preference-ktx:1.1.0-rc01"
implementation 'androidx.core:core-ktx:1.2.0-alpha02'
implementation 'androidx.collection:collection-ktx:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.2.0-alpha01'

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null)
            supportFragmentManager.commit { replace(android.R.id.content, PrefsFragment()) }
    }

    class PrefsFragment : PreferenceFragmentCompat() {
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.preferences, rootKey)
        }
    }
}

preferences.xml

<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

  <androidx.preference.Preference android:title="hello"/>
</androidx.preference.PreferenceScreen>
android developer
  • 114,585
  • 152
  • 739
  • 1,270
0

In addition to answer given by RBT, a Preference Theme must be specified otherwise the app will crash with an IllegalStateException.

In styles.xml file, just add the following line in Activity’s theme

<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
Hasan El-Hefnawy
  • 1,249
  • 1
  • 14
  • 20
  • Is a Fragment necessary? I just want a pure Activity, no fragments. I see that Google's own page and examples suggests Fragment though – Csaba Toth Apr 17 '20 at 21:09
  • @Csaba Toth It depends on your needs and what you want to achieve. If you just want to store some data and restore it later, you may need to use SharedPreferences. – Hasan El-Hefnawy Apr 18 '20 at 20:23
  • I've done custom preferences before. The good thing is that `PreferencesFragmentCompat` does the job with extremely minimal plumbing code, which is what I aim for. However adding an unnecessary fragment in itself is a plumbing code. So we are almost there if there was a `PreferencesActivityCompat` or something. Goal: No plumbing code. – Csaba Toth Apr 19 '20 at 01:24