Android 10 未満でもダークテーマ(ナイトモード)に対応する方法|Kotlin|開発裏話
Android 10 から端末に標準搭載される「ダークテーマ(ナイトモード)」は、以下のように設定アプリの「Dark theme」項目から切り替えが可能です。
実は、設定アプリにこの切り替えスイッチが存在しないだけで、Android 10 未満の端末でも「ダークテーマ(ナイトモード)」は対応が可能です。
スレッド式メモ帳アプリ『CBnotes』は、Android 10 未満の端末にインストールしても「ダークテーマ(ナイトモード)」の切り替え設定が可能になっています。
公式ガイド「ダークテーマ」
以下の公式ガイドに従って、アプリのテーマは、DayNight テーマから継承します。『CBnotes』では、「MaterialComponents のダークテーマ機能」を使用しています。
DayNight テーマを継承してしまえば、対応は非常に簡単です。
Android 10 端末の設定アプリに存在するスイッチに類するものを、アプリに独自に実装し、公式ガイドの通り、
テーマを切り替えるには、AppCompatDelegate.setDefaultNightMode() を呼び出します。
これを呼び出して「MODE_NIGHT_YES」「MODE_NIGHT_NO」を切り替えれば実現できます。
『CBnotes』では、以下のようなスイッチを搭載しました。
ダークテーマ(ナイトモード)用のカラーリソースは、独自に定義が可能です。以下のように、リソースを別途(values-night)定義して対応します。
標準(ライトテーマ)
res\values\colors.xml
ダークテーマ
res\values-night\colors.xml
実装の重要な注意点
公式ガイドに従って「AppCompatDelegate.setDefaultNightMode()」を呼び出しますが、この設定値はシステムに保存されません。
「MODE_NIGHT_YES」を設定してダークテーマ(ナイトモード)に切り替えていても、次にアプリを起動すると、ライトテーマになってしまいます。
ですので、独自にアプリ内で設定を保存しておかなければならず、また、アプリの起動時にその設定を反映しなければなりません。
その設定の反映はタイミングが非常に重要です。
公式ガイドに記載がある通り(以下)、設定の切り替えによってアクティビティが再生成されますから、アクティビティの onCreate で反映するとタイミングが遅すぎて、アクティビティが二度連続で起動するような挙動(見た目にチラツキが分かる)になってしまいます。
注:AppCompat v1.1.0 以降、開始されているアクティビティは setDefaultNightMode() により自動的に再作成されます。
アプリが起動するときの、最速の処理タイミングは、Application の onCreate です。
「ダークテーマ(ナイトモード)」の反映は、このタイミングで対応する必要があります。
『CBnotes』における実装
Application の実装は以下の通りです。この onCreate のタイミングで実装すれば、反映はスムーズで、見た目(チラツキ)の問題も起こりません。設定の保存は小規模なので SharedPreferences で十分です。
import android.app.Application
import android.content.Context
import android.preference.PreferenceManager
import androidx.appcompat.app.AppCompatDelegate
class BossApplication : Application() {
companion object {
private const val KEY_DARK_THEME = "dark_theme"
/**
* Retrieve a boolean value from the preferences.
*
* @param context The context of the preferences whose values are wanted.
* @return Returns the preference value if it exists, or defValue.
*/
fun getDarkTheme(context: Context): Boolean {
return PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(KEY_DARK_THEME, false)
}
/**
* Set a boolean value in the preferences.
*
* @param context The context of the preferences whose values are wanted.
* @param value The new value for the preference.
*/
fun putDarkTheme(context: Context, value: Boolean) {
PreferenceManager
.getDefaultSharedPreferences(context)
.edit()
.putBoolean(KEY_DARK_THEME, value)
.apply()
}
/**
* Sets the default night mode.
*
* @param isChecked The new checked state of buttonView.
*/
fun setDefaultNightMode(isChecked: Boolean) {
AppCompatDelegate.setDefaultNightMode(
if (isChecked) {
// Night mode which uses always uses a dark mode, enabling night qualified
// resources regardless of the time.
AppCompatDelegate.MODE_NIGHT_YES
} else {
// Night mode which uses always uses a light mode, enabling notnight qualified
// resources regardless of the time.
AppCompatDelegate.MODE_NIGHT_NO
}
)
}
}
override fun onCreate() {
// Sets the default night mode.
setDefaultNightMode(getDarkTheme(applicationContext))
// Called when the application is starting, before any activity, service,
// or receiver objects (excluding content providers) have been created.
super.onCreate()
}
}
スイッチでの切り替え実装は以下の通りです。
// Set an action view for this menu item.
navMenu.findItem(R.id.nav_dark_theme).actionView = Switch(this).apply {
// Changes the checked state of this button.
isChecked = BossApplication.getDarkTheme(this@MainActivity)
// Register a callback to be invoked when the checked state of this button
// changes.
setOnCheckedChangeListener { _, isChecked ->
// Calls the specified function [block] with `this` value as its receiver and returns its result.
BossApplication.run {
// Set a boolean value in the preferences.
putDarkTheme(this@MainActivity, isChecked)
// Sets the default night mode.
setDefaultNightMode(isChecked)
}
}
}
『CBnotes』ソースコード一式
以下 note で、実際に Google Play へ公開リリースしている『CBnotes』のソースコード一式を販売しております。
Android(Kotlin)アプリ開発を学習している方々には「参考教材」として、業務で動作実績のあるサンプルコード&開発環境一式が必要なプロの方々には「工数削減」として、大変にオススメです。