Apuntes para crear un cuadro de diálogo donde el usuario puede escoger el modo oscuro a aplicar en la app, se le deja escoger entre el tema claro, oscuro, definido por el sistema o bien por el ahorro de batería.

Prerequesitos
- Tener implementado el soporte de Modo oscuro, Cómo implementar el modo oscuro en Kotlin para Android
- Pantalla de preferencias
Definiendo el selector de tema
Crear los textos y valores para mostrar en el cuadro de diálogo para seleccionar el tema de modo oscuro
Archivo values/strings.xml
<string name="pref_interface_theme_title">Theme</string>
<string name="pref_interface_theme_dialog_title">Choose theme</string>
<string name="pref_interface_theme_light">Light</string>
<string name="pref_interface_theme_dark">Dark</string>
<string name="pref_interface_theme_default_system">System default</string>
<string name="pref_interface_theme_auto_battery">Set by battery saver</string>
Definir los arrays de valores archvios values/arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_theme_entries">
<item>@string/pref_interface_theme_light</item>
<item>@string/pref_interface_theme_dark</item>
<item>@string/pref_interface_theme_default_system</item>
<item>@string/pref_interface_theme_auto_battery</item>
</string-array>
<string-array name="pref_theme_entries_values">
<item>MODE_NIGHT_NO</item>
<item>MODE_NIGHT_YES</item>
<item>MODE_DEFAULT_SYSTEM</item>
<item>MODE_NIGHT_AUTO_BATTERY</item>
</string-array>
</resources>
pref_theme_entries contendrá las cadenas de textos a mostrar de cada selección:
pref_theme_entries_values el valor que se usará para controlar el modo a establecer
En el XML donde se define los elementos de la ventana de configuración añadir
<ListPreference
android:defaultValue="MODE_DEFAULT_SYSTEM"
android:dialogTitle="@string/pref_interface_theme_dialog_title"
android:entries="@array/pref_theme_entries"
android:entryValues="@array/pref_theme_entries_values"
android:key="pref_dark_theme"
android:summary="%s"
android:title="@string/pref_interface_theme_title"
app:iconSpaceReserved="false" />
defaultValue: el valor del modo oscuro por defecto en este caso MODE_DEFAULT_SYSTEM
el android:key se declara el identificador de la preferencia en este caso pref_dark_theme
Control del modo oscuro
En el inicio de la aplicación en MyApplication.kt es donde se debe comprobar que modo oscuro a establecer para la totalidad de la app
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
instance = this
prefs = Prefs(applicationContext)
when (prefs.darkTheme) {
"MODE_NIGHT_NO" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"MODE_NIGHT_YES" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"MODE_DEFAULT_SYSTEM" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"MODE_NIGHT_AUTO_BATTERY" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
}
}
companion object {
lateinit var instance: MyApplication
private set
lateinit var prefs: Prefs
private set
init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
}
}
Para el controlador de preferencias Prefs.kt
class Prefs(val context: Context) {
val PREFS_FILENAME = BuildConfig.APPLICATION_ID + ".prefs"
val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE)
fun getDefaultSharedPreferencesName(): String? = context.packageName + "_preferences"
private val sharedDefaultPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val darkTheme: String?
get() = sharedDefaultPreferences.getString("pref_dark_theme", "MODE_DEFAULT_SYSTEM")
}
Aplicar cambios al salir de configuración
En caso de querer aplicar los cambios sin la necesidad de que reinicie la aplicación el usuario, se puede interceptar el cambio de selección del cuadro de diálogo y al salir de configuración, restablecer la vista.
En el controlador de la vista de preferencias, podemos interceptar el cambio de selección con onPreferenceChangeListener
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val selectTheme: ListPreference? = findPreference("pref_dark_theme")
selectTheme?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
(activity as SettingsActivity).needRecreate = true
true
}
}
Al detectar la selección por parte del usuario, añadimos un controlador de bandera que ha habido cambio con needRecreate = true
En SettingsActivity.kt
var needRecreate: Boolean = false
...
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
return true
}
if (needRecreate) triggerRebirth(this) else finish()
return super.onSupportNavigateUp()
}
Para reinicar la actividad usaremos la función triggerRebirth()
private fun triggerRebirth(context: Context) {
val packageManager: PackageManager = context.packageManager
val intent = packageManager.getLaunchIntentForPackage(context.packageName)
val componentName = intent!!.component
val mainIntent = Intent.makeRestartActivityTask(componentName)
context.startActivity(mainIntent)
Runtime.getRuntime().exit(0)
}
Estilo para Settings
Ajustes para que la vetana de ajustes aparezca concorde con los estilos definidos en la app
<style name="AppTheme.DayNight.Settings" parent="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge">
<item name="windowActionBar">true</item>
<item name="windowNoTitle">false</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<!--<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>-->
<item name="android:statusBarColor">?attr/colorPrimary</item>
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimaryDark">@color/primaryDarkColor</item>
<item name="colorAccent">@color/secondaryColor</item>
<item name="android:navigationBarColor">?android:colorBackground</item>
</style>