Implement Dark/Night Mode in an Android app with a ListPreference toggle
Tags: android, kotlin, tutorial, java
Published: 2020-05-13 07:48:15

Cover Photo by <a href="https://unsplash.com/@glvrdru?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Maxim Ilyahov</a> on <a href="https://unsplash.com/s/photos/android-mobile-dark-mode?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>
<br>

**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`
``` 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`
``` kotlin
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`
``` 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`
``` 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`
``` 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`
``` kotlin
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`
``` kotlin
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.

{% youtube b6YJ-F66qYU %}

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`
``` 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`
``` kotlin
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.

{% youtube qMNjGRy6n-E %}

And with this, this tutorial is completed. 
Check out the full code on my GitHub here: {% github Aurum1611/DarkModePreferencesTutorial %}
Upon request, I've created a Java counterpart for the project: {% github Aurum1611/DarkModePreferenceTutorialJava %}
I hope this is helpful. Happy coding!