見出し画像

Android 10 未満でもダークテーマ(ナイトモード)に対応する方法|Kotlin|開発裏話

Android 10 から端末に標準搭載される「ダークテーマ(ナイトモード)」は、以下のように設定アプリの「Dark theme」項目から切り替えが可能です。

画像1

実は、設定アプリにこの切り替えスイッチが存在しないだけで、Android 10 未満の端末でも「ダークテーマ(ナイトモード)」は対応が可能です。

スレッド式メモ帳アプリ『CBnotes』は、Android 10 未満の端末にインストールしても「ダークテーマ(ナイトモード)」の切り替え設定が可能になっています。


公式ガイド「ダークテーマ

以下の公式ガイドに従って、アプリのテーマは、DayNight テーマから継承します。『CBnotes』では、「MaterialComponents のダークテーマ機能」を使用しています。

DayNight テーマを継承してしまえば、対応は非常に簡単です。

Android 10 端末の設定アプリに存在するスイッチに類するものを、アプリに独自に実装し、公式ガイドの通り、

テーマを切り替えるには、AppCompatDelegate.setDefaultNightMode() を呼び出します。

これを呼び出して「MODE_NIGHT_YES」「MODE_NIGHT_NO」を切り替えれば実現できます。

CBnotes』では、以下のようなスイッチを搭載しました。

画像2

ダークテーマ(ナイトモード)用のカラーリソースは、独自に定義が可能です。以下のように、リソースを別途(values-night)定義して対応します。

標準(ライトテーマ)
res\values\colors.xml

ダークテーマ
res\values-night\colors.xml


実装の重要な注意点

公式ガイドに従って「AppCompatDelegate.setDefaultNightMode()」を呼び出しますが、この設定値はシステムに保存されません。

MODE_NIGHT_YES」を設定してダークテーマ(ナイトモード)に切り替えていても、次にアプリを起動すると、ライトテーマになってしまいます。

ですので、独自にアプリ内で設定を保存しておかなければならず、また、アプリの起動時にその設定を反映しなければなりません。

その設定の反映はタイミングが非常に重要です。

公式ガイドに記載がある通り(以下)、設定の切り替えによってアクティビティが再生成されますから、アクティビティの onCreate で反映するとタイミングが遅すぎて、アクティビティが二度連続で起動するような挙動(見た目にチラツキが分かる)になってしまいます。

注:AppCompat v1.1.0 以降、開始されているアクティビティは setDefaultNightMode() により自動的に再作成されます。

アプリが起動するときの、最速の処理タイミングは、ApplicationonCreate です。

「ダークテーマ(ナイトモード)」の反映は、このタイミングで対応する必要があります。


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)アプリ開発を学習している方々には「参考教材」として、業務で動作実績のあるサンプルコード&開発環境一式が必要なプロの方々には「工数削減」として、大変にオススメです。


この記事が気に入ったらサポートをしてみませんか?