1

After creating a preferences activity, I've noticed that my main activity doesn't change themes when my checkbox preference is checked despite calling onSharedPreferenceChanged. Does anyone know what is wrong and how this can be fixed?

styles.xml

<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
    <!--<item name="android:windowBackground">@color/colorLight</item>-->
</style>

<style name="MyDarkMaterialTheme" parent="android:Theme.Material">
    <item name="android:windowBackground">@android:color/black</item>
</style>

<style name="MyLightMaterialTheme" parent="android:Theme.Material.Light.DarkActionBar">
    <item name="android:windowBackground">@color/colorLight</item>
</style>

MainActivity class

public class MainActivity extends Activity {

    boolean themeState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setTheme(R.style.MyDarkMaterialTheme);

        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_settings:
                Intent settingsIntent = new Intent(this, SettingsActivity.class);
                startActivity(settingsIntent);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }


    @Override
    public void onResume(){
        super.onResume();
        loadPreferences();
        displaySettings();
    }

    private void loadPreferences(){
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        themeState = sharedPreferences.getBoolean("pref_pref1", true);
    }

    public void displaySettings() {
        if (themeState) {
            setTheme(R.style.MyDarkMaterialTheme);
            recreate();
        } else {
            setTheme(R.style.MyLightMaterialTheme);
            recreate();
        }
    }
}

SettingsActivity class

public class SettingsActivity extends Activity {

    boolean themeState;

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

        if (savedInstanceState == null) {
            Fragment preferenceFragment = new SettingsFragment();
            FragmentTransaction ft = getFragmentManager().beginTransaction();
            ft.add(R.id.pref_container, preferenceFragment);
            ft.commit();
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            final Intent intent = getParentActivityIntent();
            if(intent != null){
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
            }
            onBackPressed();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }


    @Override
    public void onResume(){
        super.onResume();
        loadPreferences();
        displaySettings();
    }

    private void loadPreferences(){
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        themeState = sharedPreferences.getBoolean("pref_pref1", true);
    }

    public void displaySettings() {
        if (themeState) {
            getApplication().setTheme(R.style.MyDarkMaterialTheme);
            recreate();
        } else {
            getApplication().setTheme(R.style.MyLightMaterialTheme);
            recreate();
        }
    }
}

SettingsFragment class

public class SettingsFragment extends PreferenceFragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Load the Preferences from the XML file
        addPreferencesFromResource(R.xml.app_preferences);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                // Settings activity or fragment should restart with changes applied

            }
        };
    }
}

xml/app_preferences

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

    <CheckBoxPreference
        android:key="pref_pref1"
        android:title="@string/dark_theme"
        android:defaultValue="false"
        android:layout="@layout/preference_multiline"
        />

</PreferenceScreen>

Csongi77's suggestion

public class SettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Load the Preferences from the XML file
        addPreferencesFromResource(R.xml.app_preferences);

        // Find appropriate preference
        CheckBoxPreference mThemePreference =(CheckBoxPreference)getPreferenceManager().findPreference("pref_pref1");
        // we have to set up listener in order for persisting change to new value
        mThemePreference.setOnPreferenceChangeListener(this);
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mThemePreference.getContext());
        Boolean value=sharedPreferences.getBoolean("pref_pref1",true);
        onPreferenceChange(mThemePreference, value);
    }

    // overriding onPreferenceChange - if we return true, the preference will be persisted
    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        String preferenceKey = preference.getKey();
        // we have to check the preference type and key, maybe later we have more preferences....
        if(preference instanceof CheckBoxPreference){
            if(preferenceKey.equals("pref_pref1")){
                ((CheckBoxPreference)preference).setChecked((Boolean)newValue);
                // ... do other preference related stuff here - if necessary, for example setSummary, etc...
                getActivity().setTheme(R.style.MyDarkMaterialTheme);
            } else {
                getActivity().setTheme(R.style.MyLightMaterialTheme);
            }
        }
        return true;
    }
}

Logcat

          Process: com.companyname.appname, PID: 4505
          java.lang.NullPointerException: Attempt to invoke interface method 'void com.companyname.appname.SettingsFragment$PreferenceXchangeListener.onXchange(java.lang.Boolean)' on a null object reference
              at com.companyname.appname.SettingsFragment.onPreferenceChange(SettingsFragment.java:57)
              at android.preference.Preference.callChangeListener(Preference.java:928)
              at android.preference.TwoStatePreference.onClick(TwoStatePreference.java:64)
              at android.preference.Preference.performClick(Preference.java:983)
              at android.preference.PreferenceScreen.onItemClick(PreferenceScreen.java:214)
              at android.widget.AdapterView.performItemClick(AdapterView.java:300)
              at android.widget.AbsListView.performItemClick(AbsListView.java:1143)
              at android.widget.AbsListView$PerformClick.run(AbsListView.java:3044)
              at android.widget.AbsListView$3.run(AbsListView.java:3833)
              at android.os.Handler.handleCallback(Handler.java:739)
              at android.os.Handler.dispatchMessage(Handler.java:95)
              at android.os.Looper.loop(Looper.java:135)
              at android.app.ActivityThread.main(ActivityThread.java:5221)
              at java.lang.reflect.Method.invoke(Native Method)
              at java.lang.reflect.Method.invoke(Method.java:372)
              at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
              at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

SettingsActivity class

public class SettingsActivity extends Activity implements SettingsFragment.PreferenceXchangeListener {
    private static final String TAG = SettingsActivity.class.getSimpleName();

    // declaring initial value for applying appropriate Theme
    private Boolean mCurrentValue;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // Checking which Theme should be used. IMPORTANT: applying Themes MUST called BEFORE super.onCreate() and setContentView!!!
        Log.d(TAG, "onCreate:::: retrieving preferences");
        SharedPreferences mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mCurrentValue = mSharedPreferences.getBoolean("my_preference",false);
        Log.d(TAG, "onCreate:::: my_preference and mCurrentValue=" + mCurrentValue);
        if(mCurrentValue){
            // we have to use simple setTheme() instead getApplication.setTheme()!!!
            setTheme(R.style.DarkTheme);
            Log.d(TAG, "onCreate:::: setTheme:DarkTheme");
        } else {
            setTheme(R.style.LightTheme);
            Log.d(TAG, "onCreate:::: setTheme:LightTheme");
        }

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);

        Fragment preferenceFragment = new SettingsFragment();
        getFragmentManager().beginTransaction().add(R.id.preference_container, preferenceFragment).commit();
    }

    // callback method for changing preference. It's called only if "my_preference" has changed
    @Override
    public void onXchange(Boolean value) {
        // if value differs from previous Theme, we recreate Activity
        Log.d(TAG, "onXchange:::: \n has called");
        if (value!=mCurrentValue) {
            Log.d(TAG, "onXchange:::: \n new value!=oldValue");
            mCurrentValue=value;
            recreate();
        }
    }
}

SettingsFragment class

public class SettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {
    private static final String TAG = SettingsFragment.class.getSimpleName();

    // declaring PreferenceXchangeListener
    private PreferenceXchangeListener mPreferenceXchangeListener;

    public SettingsFragment() {
    }

    // declaring PreferenceXchangeListener in order to communicate with Activities
    public interface PreferenceXchangeListener {
        void onXchange(Boolean value);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Load the Preferences from the XML file
        addPreferencesFromResource(R.xml.app_preferences);

        CheckBoxPreference mCheckBoxPreference = (CheckBoxPreference)findPreference("my_preference");
        mCheckBoxPreference.setOnPreferenceChangeListener(this);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // on Attch we assign parent Activity as PreferenceXchangeListener
        try {
            mPreferenceXchangeListener = (PreferenceXchangeListener) context;
        } catch (ClassCastException e) {
            Log.e(TAG, "onAttach::::: PreferenceXchangeListener must be set in parent Activity");
        }
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        String preferenceKey=preference.getKey();
        // only my_preference is checked in this case. Later you may add another behaviour to another preference change
        if(preferenceKey.equals("my_preference")){
            ((CheckBoxPreference)preference).setChecked((Boolean)newValue);
            // executing parent Activity's callback with the new value
            mPreferenceXchangeListener.onXchange((Boolean)newValue);
            return true;
        }
        // ... check other preferences here
        return false;
    }
}
wbk727
  • 8,017
  • 12
  • 61
  • 125
  • 1
    Have you implemented PreferenceXchangeListener in SettingsActivity? Have you assigned a PreferenceXchangeListener in onAttach(Context c) method in PreferenceFragment? – Csongi77 Sep 01 '18 at 12:11
  • @Csongi77 I believe so. Please my updated Java classes **SettingsActivity** and **SettingsFragment** – wbk727 Sep 01 '18 at 12:27
  • I don't know what can cause this :/ . On my Emulator it works flawless (on Android API26)... Based on LogCat message it seems that mPreferenceXchangeListener was not initialized _or_ newValue is null. Please check this. Maybe uninstalling app could help (if SharedPreferences hasn't been modified and the new code tried to read it but with new types...) – Csongi77 Sep 01 '18 at 12:44
  • 1
    I think the problem is my emulator as it's running API 21 & it also has some performance issues anyway. Thanks for helping out. – wbk727 Sep 01 '18 at 12:51
  • Update: This solution only works on API 23+ (Marshmallow and above) – wbk727 Sep 13 '18 at 18:32

2 Answers2

1

Try this:

public class SettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //Load the Preferences from the XML file
    addPreferencesFromResource(R.xml.app_preferences);

    // Find appropriate preference
    CheckBoxPreference mThemePreference =(CheckBoxPreference)getPreferenceManager().findPreference("pref_pref1");
    // we have to set up listener in order for persisting change to new value
    mThemePreference.setOnPreferenceChangeListener(this);
    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mThemePreference.getContext());
    Boolean value=sharedPreferences.getBoolean("pref_pref1",true);
    onPreferenceChange(mThemePreference, value);
    }

// overriding onPreferenceChange - if we return 'true', the preference will be persisted
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
    String preferenceKey = preference.getKey();
    // we have to check the preference type and key, maybe later we have more preferences....
    if(preference instanceof CheckBoxPreference){
        if(preferenceKey.equals("pref_pref1")){
      // Edited this line *******               
        ((CheckBoxPreference)preference).setChecked((Boolean)newValue);   
            // ... do other preference related stuff here - if necessary, for example setSummary, etc...       
        }
    }
return true;
}

}

In a nutshell: implement OnPreferenceChange in your PreferenceFragment. When you override onPreferenceChane return true. In this case the older preference will be overwritten. Hope it helps (if yes, please don't forget to accept my answer)! Best regards, Cs

P.S: don't forget to uninstall app on Emulator

Csongi77
  • 329
  • 3
  • 13
  • A warning & error are returned after trying out your code: `Method invocation 'setOnPreferenceChangeListener' may produce 'java.lang.NullPointerException'` and `Cannot resolve method 'setChecked(java.lang.Boolean)'`. – wbk727 Aug 30 '18 at 11:46
  • I forgot to cast preference into CheckBoxPreference. I tried this version, it worked for me. However I used Activity (extended from AppCompatActivity) and PreferenceFragment was a static inner class of the Activity. – Csongi77 Aug 30 '18 at 14:19
  • Okay what about the NullPointerException part? What should that be changed to? – wbk727 Aug 30 '18 at 14:28
  • What says Logcat? Where was it thrown? – Csongi77 Aug 30 '18 at 14:30
  • I don't get any errors in the logcat but the theme doesn't change when I check or uncheck the CheckBox. See the code section **Csongi77's suggestion ** – wbk727 Aug 30 '18 at 15:01
  • @Csongi77 your code block has low readability and it makes it hard for reusing it in the future. can you make it more readable so it would have a better impact on other developers? here is something you can do: add some blank lines and separate different parts of your code and align your comments they way that is more readable. thank you :) – Mohammad Hossein Shojaeinia Mar 12 '19 at 05:14
  • @Csongi77 Can you also confirm how to add an animation when the theme changes? I've spent months try to find a working example but found nothing. I've created a [new question](https://stackoverflow.com/questions/55983269/how-to-apply-animation-for-theme-change-programmatically) for this onging issue. Would be great if also anyone know how to do this when a button is clicked as there are absolutely no working examples in Kotlin. – wbk727 Mar 10 '20 at 12:49
1

Ok, here's the working version:

1) in MainActivity.java's onCreate method check current Theme before calling super.onCreate() and setContentView() and add it in a private global boolean variable (let's call it mTheme). Further info: Change Activity's theme programmatically

2) in MainActivity.java's onStart() method you should check whether the settings has been changed because onCreate won't called when you return from another Activity. If mTheme!=newSettingValue, call recreate(). Important: it's similar to onDestroy method, so previously set values may lost! https://developer.android.com/reference/android/app/Activity#recreate()

3) In your SettingsFragment you have to define an interface (ThemeXchangeListener) with an update(Boolean value) method. You also have to declare ThemeXchangeListener mListener field, too.

4) In SettingsFragment's onAttach(Context context) method assign context to mListener -> mListener=(ThemeXchangeListener)context;

5) In SettingsFragment's onPreferenceChange(Preference pref, Object value) call mListener.update((Boolean)value);

6) In SettingsActivity declare boolean for storing current Theme value(boolean mTheme). In onCreate() load preference and assign value for it. Before super.onCreate() and setContentView() assign appropriate Theme.

7) In SettingsActivity implement ThemeXchangeListener and override update(Boolean value) method. If value!=mTheme, call recreate() method. This will update your Theme at once.

You may check the full working code (with comments) at: https://github.com/csongi77/UpdateThemeOnPreferenceChange

It works fine on my Emulator from API15(IceCreamSandwich). Please let me know whether it works! Best regards, Cs

Csongi77
  • 329
  • 3
  • 13