Search code examples
androidsharedpreferencesandroid-themeandroid-preferences

My ListPreference is not changing App Theme during runtime


How can I get my Activity to recreate itself and remain a particular theme whenever I choose an item within a ListPreference to change the theme of my app? For some reason, my app is stuck on the light theme, regardless of whichever item in the ListPreference is chosen. I am not sure what I've done wrong here.

styles.xml

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light"/>

    <style name="MyDarkSettingsTheme" parent="Theme.AppCompat"/>

    <style name="MyLightSettingsTheme" parent="Theme.AppCompat.Light"/>

</resources>

Activity

class SettingsActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
    // Declaring initial value for applying appropriate Theme
    private var mCurrentValue: Boolean = true // True is the default value

    override fun onCreate(savedInstanceState: Bundle?) {
        // Checking which Theme should be used. IMPORTANT: applying Themes MUST called BEFORE super.onCreate() and setContentView!!!
        val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        mCurrentValue = mSharedPreferences.getBoolean("light", true)
        if (mCurrentValue) setTheme(R.style.MyLightSettingsTheme)
        else setTheme(R.style.MyDarkSettingsTheme)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.settings_activity)
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.settings, SettingsFragment())
            .commit()

        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        sharedPreferences.registerOnSharedPreferenceChangeListener(this)
    }

    // In order to recreate Activity, we must check the value here. Because, when we come back from another Activity, the onCreate isn't called again.
    override fun onStart() {
        super.onStart()

        setContentView(R.layout.settings_activity)
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.settings, SettingsFragment())
            .commit()

        val mFrameLayout = findViewById<FrameLayout>(R.id.settings)

        val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        val mNewValue = mSharedPreferences.getBoolean("light", true)
        // If value differs from previous Theme, recreate Activity
        if (mCurrentValue != mNewValue) recreate()

//        if (mNewValue) {
//            mFrameLayout.setBackgroundColor(Color.WHITE)
//        }
//        else {
//            mFrameLayout.setBackgroundColor(Color.BLACK)
//        }

        // ... do other stuff here

    }

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

    override fun onDestroy() {
        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
        //...
        super.onDestroy()
    }

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        when (key) {
            "list_theme" -> {
                if (sharedPreferences.getString(key, "light") == "light") {
                    setTheme(R.style.MyLightSettingsTheme)
                    recreate()
                }
                else if (sharedPreferences.getString(key, "dark") == "dark") {
                    setTheme(R.style.MyDarkSettingsTheme)
                    recreate()
                }
            }
        }
    }
}

1) Activity opened

enter image description here

2) ListPreference click

enter image description here

3) Dark preference selected

enter image description here

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }


    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_settings -> {
                val intentSettings = Intent(this, SettingsActivity ::class.java)
                startActivity(intentSettings)
                true
            }

            else ->
                super.onOptionsItemSelected(item)
        }
    }
}

Solution

  • First, you need to add more colors/themes in your styles.xml file.

    Example:

    <!-- Base Light application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    
    <!--Dark Theme Default-->
    <style name="AppThemeDark" parent="Theme.AppCompat">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    

    In my Base Application class, I have added these 3 methods. My app has 3 themes, if you have more than that, you can edit the code accordingly.

    @Override
    public void onCreate() {
        super.onCreate();
        PreferenceManager.setDefaultValues(this, R.xml.pref_main, false);
        Context context = this;
        context.setTheme(getUserTheme());
        mInstance = this;
        res = getResources();
        shared = PreferenceManager.getDefaultSharedPreferences(context);
    }
    
    public static int getTheme(Context context, int light, int dark, int amoled) {
        String darkTheme = context.getString(R.string.dark_mode_theme);
        String amoledTheme = context.getString(R.string.amoled_mode_theme);
        shared = PreferenceManager.getDefaultSharedPreferences(context);
        String theme = shared.getString(Constants.PREFERENCE_THEME, darkTheme);
        //Able to access these values outside the class!
        if (theme.equals(darkTheme)) {
            return dark;
        } else if (theme.equals(amoledTheme)) {
            return amoled;
        } else {
            return light;
        }
    }
    
    private int getUserTheme() {
        return getTheme(this, R.style.AppTheme, R.style.AppThemeDark, R.style.AppThemeAMOLED);
    }
    

    If you don't have a class that extends Application, create it and then add it to the AndroidMainfest by putting this line in the <Application> Tag

     android:name="App"
    

    I also added the following methods in a Base Activity Class. All of my activities extend this class. If your app only has a single activity, this is less relevant to you.

    protected abstract int getAppTheme(); //has to be implemented in anything that extends this class
    
    protected void setAppTheme(int id) {
        super.setTheme(id);
        themeId = id;
    }
    
     protected void onResume() {
            super.onResume()
            if (themeId !== getAppTheme())
            {
                recreate();
            }
        }
    

    After you change the theme, you need to recreate the activity to activate the changes.

    recreate();
    

    You cannot call it from a static method, unfortunately. I'd also recommend checking the theme in onCreate and if the current theme doesn't match the user-selected theme, then change the theme and then call recreate(); .

    Next, in a regular activity that extends the base activity class, I added this. Make sure to keep it in the same order, everywhere. If you don't, the themes will be mismatched.

     public int getAppTheme() {
            return App.getTheme(this, R.style.AppTheme, R.style.AppThemeDark, R.style.AppThemeAMOLED);
        }