DEV Community 👩‍💻👨‍💻

Cover image for Implement Dark/Night Mode in an Android app with a ListPreference toggle
Neeyat Lotlikar
Neeyat Lotlikar

Posted on • Updated on

Implement Dark/Night Mode in an Android app with a ListPreference toggle

Cover Photo by Maxim Ilyahov on Unsplash

Hello, friends!

To introduce myself, I am Neeyat Lotlikar, an Android Developer. I am currently working on my portfolio and I'd like to share what I've learned recently while working on it. This is my first post so feel free to make any corrections and suggestions.

Without any further delay, let's get going!

The first step to enable dark mode is to change your app theme to Theme.AppCompat.DayNight or Theme.MaterialComponents.DayNight if you're using the Material Design library.

res/values/styles.xml

<style name="AppTheme" parent="Theme.AppCompat.DayNight">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

Next, create a Preference activity.
This can be done with the help of the Android Studio templates with ease or you can even do it manually.

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.settings_activity)
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.settings, SettingsFragment())
            .commit()
    }

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

We want a ListPreference in our root_prefernces.xml file and so it should look something like this:

res/values/xml/root_preferences.xml

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

    <PreferenceCategory app:title="@string/ui">

        <ListPreference
            app:defaultValue="@string/dark_mode_def_value"
            app:entries="@array/dark_mode_entries"
            app:entryValues="@array/dark_mode_values"
            app:key="@string/dark_mode"
            app:title="@string/dark_mode" />

    </PreferenceCategory>

</PreferenceScreen>

Here, the app:defaultValue is used to set a default value which is stored in strings.xml. The app:title and app:key attribute values specify the display title of the preference and the key to identify it by respectively.

res/values/strings.xml

<resources>
    <!-- Preference Titles -->
    <string name="ui">UI</string>
    <string name="dark_mode">Dark Mode</string>
    <string name="dark_mode_def_value">MODE_NIGHT_FOLLOW_SYSTEM</string>
</resources>

app:entries and app:entryValues attributes take array values that specify the corresponding display name and the value it carries so make sure they are in the proper order. I have added my arrays to arrays.xml as below:

res/values/arrays.xml

<resources>
    <!-- Dark Mode ListPreference -->
    <string-array name="dark_mode_entries">
        <item>Follow System Dark Mode</item>
        <item>Light Mode Selected</item>
        <item>Dark Mode Selected</item>
        <item>Auto Battery Dark Mode</item>
    </string-array>

    <string-array name="dark_mode_values">
        <item>MODE_NIGHT_FOLLOW_SYSTEM</item>
        <item>MODE_NIGHT_NO</item>
        <item>MODE_NIGHT_YES</item>
        <item>MODE_NIGHT_AUTO_BATTERY</item>
    </string-array>

</resources>

Now that we are done with the layout, let's look at the code.

The AppCompatDelegate class' setDefaultNightMode() function is used to change the night mode settings. Following values can be passed to this method:
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - This will toggle the night mode based on whether the system-wide dark mode is enabled or not.
AppCompatDelegate.MODE_NIGHT_NO - Light mode will be enabled.
AppCompatDelegate.MODE_NIGHT_YES - Dark/Night mode will be enabled.
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - Dark/Night mode will be enabled when the device is on the battery saving mode.

So where do we use this method?
Implement SharedPreferences.OnSharedPreferenceChangeListener in SettingsActivity and override the onSharedPreferenceChanged() function.

SettingsActivity.kt

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
        val darkModeString = getString(R.string.dark_mode)
        key?.let {
            if (it == darkModeString) sharedPreferences?.let { pref ->
                val darkModeValues = resources.getStringArray(R.array.dark_mode_values)
                when (pref.getString(darkModeString, darkModeValues[0])) {
                    darkModeValues[0] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
                    darkModeValues[1] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
                    darkModeValues[2] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
                    darkModeValues[3] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
                }
            }
        }
    }

As you can see, I retrieve the preference in the above code and use it to determine which option is selected in the list preference. Using this, the night mode settings are changed.

Now, a very important thing to do is registering and unregistering the OnSharedPreferenceChangeListener. This will ensure that the changes are made almost instantly on user input and it also prevents memory leakage.
Register the listener in the onCreate() function and unregister it in the onDestroy() function.

SettingsActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.settings_activity)
    supportFragmentManager
        .beginTransaction()
        .replace(R.id.settings, SettingsFragment())
        .commit()
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

    PreferenceManager.getDefaultSharedPreferences(this)
        .registerOnSharedPreferenceChangeListener(this)
}

override fun onDestroy() {
    super.onDestroy()
    PreferenceManager.getDefaultSharedPreferences(this)
        .unregisterOnSharedPreferenceChangeListener(this)
}

Now, a provision has to be made to make navigation to the SettingsActivity possible which I did by adding an action to the options menu. You can use whatever works best for you.

It is possible to add separate icons, colors, styles, and other resources to be used in the day and the night mode. You do so by adding these resources in resource folders with the extension of -night. For example, drawable-night or values-night.

And with this, we've successfully got our dark mode toggle working.

There's a problem, however. As you can see, the ListPreference doesn't show which option is currently selected in the summary. How do we fix that? Well, that's very simple actually. There are just two things that need to be done here:
Firstly, set app:useSimpleSummaryProvider attribute value to true in ListPreference.

res/xml/root_preferences.xml

<ListPreference
    app:defaultValue="@string/dark_mode_def_value"
    app:entries="@array/dark_mode_entries"
    app:entryValues="@array/dark_mode_values"
    app:key="@string/dark_mode"
    app:title="@string/dark_mode"
    app:useSimpleSummaryProvider="true" />

Secondly, implement the Preference.SummaryProvider<ListPreference> interface in SettingsActivity and override it's provideSummary() function.

SettingsActivity.kt

override fun provideSummary(preference: ListPreference?): CharSequence =
        if (preference?.key == getString(R.string.dark_mode)) preference.entry
        else "Unknown Preference"

Note that I am using androidx.preference.Preference. SummaryProvider is not available in android.preference.Preference i.e. the android package.

And with this, this tutorial is completed.
Check out the full code on my GitHub here:

GitHub logo Aurum1611 / DarkModePreferencesTutorial

A tutorial app as a guide for implementing Night/Dark Mode using ListPreference in Android.



Upon request, I've created a Java counterpart for the project:




GitHub logo

Aurum1611
/
DarkModePreferenceTutorialJava



Part of a tutorial that shows how to create Dark Mode in Android. This project is written in Java and is a counterpart to DarkModePreferenceTutorial, the Kotlin version.






I hope this is helpful. Happy coding!

Top comments (9)

Collapse
 
remihin profile image
RemiHin

Love the post, but i cant seem to get it to work. Could you assist?

i get the: "'onSharedPreferenceChanged' overrides nothing" error

also:

        .registerOnSharedPreferenceChangeListener(this)

gives me: Type mismatch.
Required:
SharedPreferences.OnSharedPreferenceChangeListener!
Found:
SettingsActivity

Collapse
 
aurumtechie profile image
Neeyat Lotlikar Author

Have you implemented the interface? Make sure you have implemeted the SharedPreferences.OnSharedPreferenceChangeListener interface in the SettingsActivity.
If you're using the same code, I believe the second problem should go away with the same correction.
Sorry for the late reply. Wasn't online these days... Let me know how it goes.

Collapse
 
remihin profile image
RemiHin

Yes, thanks, i figured it out. I'm pretty new to Kotlin, so i wasn't clear on "how to implement". Eventually i figured out it was just a comma seperator.
class SettingsActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener

otherwise very grateful for the explanation. Thanks

Thread Thread
 
aurumtechie profile image
Neeyat Lotlikar Author

I'm glad you got it. All the best!

Collapse
 
fenxe profile image
fenxe

Thank you for this. There is a problem. If you select dark mode, force-close the app and reopen the app, it automatically changes to light mode. The option selected was still in dark mode though.

Collapse
 
aurumtechie profile image
Neeyat Lotlikar Author

You will have to also use shared preferences inside your startup/launcher activity to set the dark mode to your desired setting once the activity is created. I have made these changes in the github repo. Take a look. Hope it's helpful.

Collapse
 
fenxe profile image
fenxe

Awesome. One more thing, will the code change if i want to use SwitchPreference?

Thread Thread
 
aurumtechie profile image
Neeyat Lotlikar Author

Not much really. Even the methods used should remain the same. Switch preference will have to be implemented and that's pretty much it.

Thread Thread
 
fenxe profile image
fenxe

Okay, that really helps.

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.