36

Trying the different preference activities in the ApiDemos for Android 4.0, I see in the code that some methods are deprecated in PreferencesFromCode.java, for example.

So my question is: if I use PreferenceFragment, will it work for all version or only 3.0 or 4.0 and up?

If so, what should I use that works for 2.2 and 2.3 as well?

just_user
  • 11,769
  • 19
  • 90
  • 135
  • 2
    Not Preferences related for now, but Google started to give useful info about compatibility issues with pre Honeycomb devices in the Developer's site: http://developer.android.com/training/backward-compatible-ui/index.html – Jose_GD Apr 25 '12 at 13:49
  • There is a third party backport for PreferenceFragment. See [my answer](http://stackoverflow.com/a/22462743/1747491). – theblang Mar 17 '14 at 18:46
  • As @pcans commented below, the answer is now in official Android documentation: http://developer.android.com/guide/topics/ui/settings.html#BackCompatHeaders – Kevin Cooper Oct 27 '14 at 22:15

6 Answers6

59

PreferenceFragment will not work on 2.2 and 2.3 (only API level 11 and above). If you want to offer the best user experience and still support older Android versions, the best practice here seems to be to implement two PreferenceActivity classes and to decide at runtime which one to invoke. However, this method still includes calling deprecated APIs, but you can't avoid that.

So for instance, you have a preference_headers.xml:

<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" > 
    <header android:fragment="your.package.PrefsFragment" 
        android:title="...">
        <extra android:name="resource" android:value="preferences" />
    </header>
</preference-headers>

and a standard preferences.xml (which hasn't changed much since lower API levels):

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

Then you need an implementation of PreferenceFragment:

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

And finally, you need two implementations of PreferenceActivity, for API levels supporting or not supporting PreferenceFragments:

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

and:

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

At the point where you want to display the preference screen to the user, you decide which one to start:

if (Build.VERSION.SDK_INT < 11) {
    startActivity(new Intent(this, PreferencesActivity.class));
} else {
    startActivity(new Intent(this, OtherPreferencesActivity.class));
}

So basically, you have an xml file per fragment, you load each of these xml files manually for API levels < 11, and both Activities use the same preferences.

Jorgesys
  • 124,308
  • 23
  • 334
  • 268
Leo
  • 37,640
  • 8
  • 75
  • 100
  • insightful answer. Really needed as Google failed to give us proper documentation to make Android apps backwards compatible. Only recently things started to change though (see comment above) – Jose_GD Apr 25 '12 at 13:43
  • 3
    Good idea, only problem is when you are not the one starting the Activity, thus you cannot make the decision which to start. That, and you are using API-level detection as opposed to feature detection (using ClassLoaders to check if PreferenceFragment exists). User who edit their build.prop file to go up from API 10 to 11 (or higher) would have a crash. – Tom Jul 06 '12 at 13:49
  • 3
    This is now documented here: http://developer.android.com/guide/topics/ui/settings.html#BackCompatHeaders – pcans Aug 23 '12 at 10:59
  • @Tom You could work around the "not the one starting the Activity" problem by using a shim activity which does nothing but start the new prefs activity, then calls finish. And of course it should stay out of history/backstack. – Josh Oct 21 '13 at 13:51
  • Good solution, thank you. Unfortunately it does not work when you need to pass some programatically generated arguments to the preferences fragment with intent. – Andrey Chernih Nov 22 '13 at 14:42
  • Why do I need the PreferenceFragment? – Philipp Jahoda Feb 21 '14 at 17:18
  • Starting in API 19, please add to your class that extended PreferenceActivity: protected boolean isValidFragment(String fragmentName) { return ClassImplementation.class.getName().equals(fragmentName); } otherwise you will get an exception. Security issue. – Nick Oct 19 '14 at 16:14
18

@Mef Your answer can be simplified even more so that you do not need both of the PreferencesActivity and OtherPreferencesActivity (having 2 PrefsActivities is a PITA).

I have found that you can put the onBuildHeaders() method into your PreferencesActivity and no errors will be thrown by Android versions prior to v11. Having the loadHeadersFromResource() inside the onBuildHeaders did not throw and exception on 2.3.6, but did on Android 1.6. After some tinkering though, I found the following code will work in all versions so that only one activity is required (greatly simplifying matters).

public class PreferencesActivity extends PreferenceActivity {
    protected Method mLoadHeaders = null;
    protected Method mHasHeaders = null;

    /**
     * Checks to see if using new v11+ way of handling PrefFragments.
     * @return Returns false pre-v11, else checks to see if using headers.
     */
    public boolean isNewV11Prefs() {
        if (mHasHeaders!=null && mLoadHeaders!=null) {
            try {
                return (Boolean)mHasHeaders.invoke(this);
            } catch (IllegalArgumentException e) {
            } catch (IllegalAccessException e) {
            } catch (InvocationTargetException e) {
            }
        }
        return false;
    }

    @Override
    public void onCreate(Bundle aSavedState) {
        //onBuildHeaders() will be called during super.onCreate()
        try {
            mLoadHeaders = getClass().getMethod("loadHeadersFromResource", int.class, List.class );
            mHasHeaders = getClass().getMethod("hasHeaders");
        } catch (NoSuchMethodException e) {
        }
        super.onCreate(aSavedState);
        if (!isNewV11Prefs()) {
            addPreferencesFromResource(R.xml.preferences);
            addPreferencesFromResource(R.xml.other);
        }
    }

    @Override
    public void onBuildHeaders(List<Header> aTarget) {
        try {
            mLoadHeaders.invoke(this,new Object[]{R.xml.pref_headers,aTarget});
        } catch (IllegalArgumentException e) {
        } catch (IllegalAccessException e) {
        } catch (InvocationTargetException e) {
        }   
    }
}

This way you only need one activity, one entry in your AndroidManifest.xml and one line when you invoke your preferences:

startActivity(new Intent(this, PreferencesActivity.class);

UPDATE Oct 2013: Eclipse/Lint will warn you about using the deprecated method, but just ignore the warning. We are using the method only when we have to, which is whenever we do not have v11+ style preferences and must use it, which is OK. Do not be frightened about Deprecated code when you have accounted for it, Android won’t remove deprecated methods anytime soon. If it ever did occur, you won’t even need this class anymore as you would be forced to only target newer devices. The Deprecated mechanism is there to warn you that there is a better way to handle something on the latest API version, but once you have accounted for it, you can safely ignore the warning from then on. Removing all calls to deprecated methods would only result in forcing your code to only run on newer devices — thus negating the need to be backward compatible at all.

Uncle Code Monkey
  • 1,796
  • 1
  • 14
  • 23
  • 3
    The exception you had on 1.6 is due to Dalvik trying to load all the methods (or checking for their existence) even if they're not called at runtime. This behavior has been changed in 2.0, so that's why there is no exception in 2.3.6. – bigstones Aug 21 '12 at 14:25
  • @Unclude Code Monkey Where do you use the preferenceFragment xml in there? – Maxrunner Sep 20 '12 at 13:44
  • @Maxrunner In onBuildHeaders() you would replace the "R.xml.pref_headers" with the R int referencing your preferenceHeaders.xml file. Typically, you should only have one such file, otherwise just list the "main" one. In onCreate() where the addPreferencesFromResource() lines are located, that is where you would list all the R int references to your various preferenceFragment.xml files. Simple settings may only have one file, but this is where you would commonly have many such prefs fragment files. Just add a line for all the ones your app contains. – Uncle Code Monkey Sep 21 '12 at 15:12
  • @UncleCodeMonkey: Nice idea... Still, being a `PreferenceActivity`, methods like `findPreference()` will be deprecated, right? So I don't understand why your idea is different from just using a pre API11 `PreferenceActivity`, `PreferenceScreen`, etc... `(Preference) findPreference("SOMEPREF");` returns null for me. I think your `PreferencesActivity` should need both a `PreferenceFragment` and a `PreferenceActivity` inside a main `PreferenceActivty`, no? – Luis A. Florit Oct 27 '13 at 16:36
  • 1
    @LuisA.Florit: see my updated answer about deprecated methods. My answer is difference because it uses a unified class and all the same XML files for both Android <11 and >=11; which in turn reduces the chance for bugs when one file gets updated and the others do not. – Uncle Code Monkey Oct 27 '13 at 16:59
  • @UncleCodeMonkey: Yes, I understand what this code should do, and that's precisely why I am interested. But I can't find anything written by you about deprecated methods. I still get lots of deprecation warnings (and that makes sense to me). I don't understand where you're using `PreferenceFragment` class here. – Luis A. Florit Oct 27 '13 at 17:40
  • @UncleCodeMonkey: ok, I see now what you just wrote about deprecated methods. Will think about this, but still don't understand where the `PreferenceFragment` class enters here. It looks to me that all your code just uses pre API12 stuff... – Luis A. Florit Oct 27 '13 at 17:47
  • @LuisA.Florit: onBuildHeaders() is v11+ and automatically called by v11+ PreferenceActivity, which will invoke the new loadHeadersFromResource() method passing in the R.xml.pref_headers resource ID. Older Android versions will ignore the \@Override annotation and treat the onBuildHeaders() as a new method that is never called. – Uncle Code Monkey Oct 28 '13 at 15:23
  • @UncleCodeMonkey: Yes, I understand that, but that is just to load the layout. Yet, I think that the methods being called in your code are those of `PreferenceActivity` instead of `PreferenceFragment`. For example, in your code, the `findPreference()` is the one from `PreferenceActivity`, and that is why you get the same deprecation warnings. In other words, I really don't see how your code works with `PreferenceFragment`. In fact, you never extended the class. – Luis A. Florit Oct 28 '13 at 16:03
  • "PreferenceFragment will not work on 2.2 and 2.3 (only API level 11 and above). If you want to offer the best user experience and still support older Android versions, the best practice here seems to be to implement PreferenceActivity class(es). However, this method still includes calling deprecated APIs, but you can't avoid that." If you feel this answer insufficient, please post a better answer so we may all benefit. I'd be interested in such a solution, too. – Uncle Code Monkey Oct 29 '13 at 16:54
6

There's a newish lib that might help.

UnifiedPreference is a library for working with all versions of the Android Preference package from API v4 and up.

scottyab
  • 23,621
  • 16
  • 94
  • 105
5

Problem with previous answers is that it will stack all preferences to a single screen on pre-Honecomb devices (due to multiple calls of addPreferenceFromResource()).

If you need first screen as list and then the screen with preferences (such as using preference headers), you should use Official guide to compatible preferences

David Vávra
  • 18,446
  • 7
  • 48
  • 56
2

I wanted to point out that if you start at http://developer.android.com/guide/topics/ui/settings.html#PreferenceHeaders and work your way down to the section for "Supporting older versions with preference headers" it will make more sense. The guide there is very helpful and does work well. Here's an explicit example following their guide:

So start with file preference_header_legacy.xml for android systems before HoneyComb

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference 
    android:title="OLD Test Title"
    android:summary="OLD Test Summary"  >
    <intent 
        android:targetPackage="example.package"
        android:targetClass="example.package.SettingsActivity"
        android:action="example.package.PREFS_ONE" />
</Preference>

Next create file preference_header.xml for android systems with HoneyComb+

<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header 
    android:fragment="example.package.SettingsFragmentOne"
    android:title="NEW Test Title"
    android:summary="NEW Test Summary" />
</preference-headers>

Next create a preferences.xml file to hold your preferences...

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
        <CheckBoxPreference
        android:key="pref_key_auto_delete"
        android:summary="@string/pref_summary_auto_delete"
        android:title="@string/pref_title_auto_delete"
        android:defaultValue="false" />
</PreferenceScreen>

Next create the file SettingsActivity.java

package example.project;
import java.util.List;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;

public class SettingsActivity extends PreferenceActivity{
final static String ACTION_PREFS_ONE = "example.package.PREFS_ONE";

@SuppressWarnings("deprecation")
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    String action = getIntent().getAction();
    if (action != null && action.equals(ACTION_PREFS_ONE)) {
        addPreferencesFromResource(R.xml.preferences);
    }
    else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
        // Load the legacy preferences headers
        addPreferencesFromResource(R.xml.preference_header_legacy);
    }
}

@SuppressLint("NewApi")
@Override
public void onBuildHeaders(List<Header> target) {
    loadHeadersFromResource(R.xml.preference_header, target);
}
}

Next create the class SettingsFragmentOne.java

package example.project;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.preference.PreferenceFragment;

@SuppressLint("NewApi")
public class SettingsFragmentOne extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.preferences);
}
}

AndroidManifest.xml, added this block between my <application> tags

<activity 
   android:label="@string/app_name"
   android:name="example.package.SettingsActivity"
   android:exported="true">
</activity>

and finally, for the <wallpaper> tag...

<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/description"
android:thumbnail="@drawable/ic_thumbnail"
android:settingsActivity="example.package.SettingsActivity"
/>
EnzoAtari
  • 21
  • 1
1

I am using this library, which has an AAR in mavenCentral so you can easily include it if you are using Gradle.

compile 'com.github.machinarius:preferencefragment:0.1.1'

theblang
  • 10,215
  • 9
  • 69
  • 120