Androidアプリでちょっとした設定値を保存するには、SharedPreferencesを利用するのが一般的だと思います。
ただ、安直に利用しているとアプリが大きくなるにつれて管理が難しくなっていきます。初期段階から将来を見越した管理方法を考えておいた方がよいでしょう。
管理方法について私の持論や、実践している管理方法について紹介します。(異論は認める、というか穴だらけだと思うので突っ込み歓迎)
実践についてはこの辺参照:
https://github.com/ohmae/DmsExplorer/tree/develop/mobile/src/main/java/net/mm2d/dmsexplorer/settings
https://github.com/ohmae/VoiceMessageBoard/tree/develop/app/src/main/java/net/mm2d/android/vmb/settings
SharedPreferencesの仕組み
その前に、SharedPreferencesはどういう仕組みで設定を保存しているのかを軽く説明します。
SharedPreferencesのインスタンスは、PreferenceManager#getDefaultSharedPreferences(Context)
で取得することができますが、他にContext#getSharedPreferences(String, int)
など、名前を指定したSharedPreferencesを使うこともできます。(第二引数はすでに意味を失っているので第二引数なしのAPIができててもいい気がする)
Context#getSharedPreferences(String, int)
の第一引数がそのままファイル名として利用されてファイルに書き出されます。
PreferenceManager#getDefaultSharedPreferences(Context)
の実装は以下のようになっていて、「<アプリのパッケージ名>_preferences」をContext#getSharedPreferences(String, int)
の第一引数に指定しています。
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
private static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
SharedPreferencesが読み書きしているファイル
デバッグモードのアプリであればadbコマンドでアプリ用ディレクトリ以下をみることができるので、一度みてみるとよいでしょう。SharedPreferencesの保存ファイルはアプリ用ディレクトリ以下にある shared_prefs の下に保存されます。
$ adb shell run-as <アプリのパッケージ名> ls -la /data/data/<アプリのパッケージ名>/shared_prefs
total 4
-rw-rw---- 1 u0_a96 u0_a96 1943 2018-06-10 02:59 <アプリのパッケージ名>_preferences.xml
ファイル名は、PreferenceManager#getDefaultSharedPreferences(Context)
で取得するSharedPreferencesの場合は、<アプリのパッケージ名>_preferences.xml、Context#getSharedPreferences(String, int)
を使った場合は、第一引数に指定した名前にxmlの拡張子をつけたものになります。
拡張子からわかるように中身はxmlです。
実際に中身をみてみましょう。
コード上から以下のように書き出した場合の結果です。
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putBoolean("boolean", true)
.putInt("int", 0)
.putLong("long", 0L)
.putFloat("float", 0f)
.putString("string", "string")
.putStringSet("string-set", setOf("string", "set"))
.apply()
$ adb shell run-as <アプリのパッケージ名> cat /data/data/<アプリのパッケージ名>/shared_prefs/<ファイル名>
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="boolean" value="true" />
<string name="string">string</string>
<float name="float" value="0.0" />
<set name="string-set">
<string>set</string>
<string>string</string>
</set>
<int name="int" value="0" />
<long name="long" value="0" />
</map>
このように非常にシンプルなxmlとして書き出されます。
SharedPreferencesの実装
SharedPreferencesはインターフェースなので実装は隠蔽されていますが、実装クラス(SharePreferencesImpl.java)もSDKの中にあるので簡単に読むことができます。どういう実装になっているのか、見ておいても損はないでしょう。
ここから本題です。
DefaultSharedPreferencesを使うべきか?
前述の通り、PreferenceManager#getDefaultSharedPreferences(Context)
というデフォルトのファイル名を指定するメソッドが用意されている一方、任意のファイル名を指定することもできます。
このDefaultSharedPreferencesを使うべきか否かですが、アプリとしての基本的な設定はこれを使った方がいいと思います。積極的な理由はありませんが、単一のファイルに書き出すだけなら、わざわざ標準外の名前をつける必要がありません。標準から離れる積極的理由がないなら標準に従うべきでしょう。
一方で、アプリ以外、というと少し変ですが、ライブラリなど、アプリ本体のコード以外からSharedPreferencesを使う場合は、DefaultSharedPreferencesを使うのは絶対に避けましょう。アプリ本体とライブラリ、お互いが目隠し状態で同じファイルを読み書きするという状態なのでリスクしかないですね。
必ず、そのライブラリのGroupIDなどをプレフィックスにするなど、アプリ本体が使うファイル名と重複することが無いように管理する必要があります。
質の悪いライブラリだとDefaultSharedPreferencesを使っている可能性はあるので、それを避けるためにデフォルトをあえて避けるという判断はありかもしれません。
DefaultSharedPreferences以外は使うべきで無いか?
必要に応じて使い分けましょう。
アプリの設定値にはずっと残しておく必要のあるもの以外に、ABテストを行うための保存値など、一時的にのみ利用し将来残していく必要のない性質の値もあるでしょう。これをずっと残し続けていく値と一緒に保存してしまうと、不要になった値をクリアする処理が面倒になります。残しておいてもそれほど害はないとは言え、シンボルの重複を避けるために、その使われていないシンボルを残し続ける必要があり、精神衛生上よくないです。
短期間のみ使用し、将来にわたって残す必要の無いデータだけを別のファイルに分離しておけば、定期的に内容をクリアすることで不要になった設定値を将来に持ち越さなくてもよくなります。
また、ライブラリがアプリとは別に設定値を保持したい場合は、アプリ本体のファイルに混ぜるといろいろと問題が起きるので名前が衝突しないようにしておく必要があるでしょう。
ただ、分割するといっても設定値一つにつき1ファイルみたいな極端な分割をすると、今度はそのファイル名の重複や命名規則に頭を悩ませる必要が出てきます。
一度作ったSharedPrefenrecesファイルを削除するのは難しいため、安易に分割すると将来にわたってゴミが残り続ける可能性があるということも考えておく必要があります。(Context#deleteSharedPreferences(String)
はAPI level 24である。「えっ、無かったの!?」というメソッド)
削除自体はできたとしても、過去書き込んでいたファイルのことを完全に忘れてしまえるのは、市場から当時のバージョンを使用しているユーザーがいなくなり、削除するようになったバージョンが完全に浸透し、全ユーザがそれを実行した後、となるので、数年間は残り続けてしまう覚悟が必要です。
分割する場合は、将来不要になる可能性が十分に低い程度の粒度で、その設定値の性質ごとの適切なまとまりで分割するようにしましょう。
また、長期的なアプリの健全性以外に、SharedPreferencesがXMLでシリアライズされていることから分かるように、部分的な読み書きができず、全読み出し、全書き出しが行われます。設定項目が多くなりすぎると読み書きに時間がかかってしまいます。特に書き出しは書き出し処理の実装にかかわらずUIスレッドをブロックする可能性があるため、大きくなりすぎないように分割しましょう。この観点では頻繁に変化する値と、滅多に変化しない値を分離するのも良いでしょう。
SharedPreferencesのファイル名はどのようにつけるべきか
SharedPreferencesのファイル名は任意の名前をつけることができます。デフォルトのファイル名は「<アプリのパッケージ名>_preferences.xml」となっているように、単に「hogehoge.xml」という名前ではなく、アプリのパッケージ名をプレフィックスにつけるなど、名前の衝突が発生しにくい名前をつけるようにしましょう。
SharedPreferencesを使うのが自分たちが書いているコードだけとは限りません。使用しているライブラリも設定値を保存するために利用している可能性があります。本来ライブラリ側はアプリ本体とコンフリクトが発生しないように、そのライブラリのグループIDなどを元にした、コンフリクトが発生しにくい名前をつけているはずですが、質の悪いライブラリはそのような配慮をしていない可能性があります。
また、ライブラリを開発しているのであれば、単純な名前はもちろん、アプリのパッケージ名を使ってしまわないように注意しましょう。アプリのパッケージ名を使ってしまうとコンフリクトの危険性が高まります。
SharedPreferencesへの操作はどこで行うべきか?
SharedPreferencesのインスタンスはContextが使えるところであればどこでも取得することができますが、SharedPreferencesのインスタンスを直接操作するのは避け、処理をカプセル化すべきでしょう。ある意味当たり前と言えば当たり前ですが。
SharedPreferencesのインターフェースは自由度が高すぎるため、直接読み書きしていると収拾がつかなくなってしまいます。SharedPreferencesにアクセスしてよいクラスを限定し、特定のパッケージ下で実装等を隠蔽、集約する仕組みを作った方がよいでしょう。
また、当然ですがSharedPreferencesのインスタンスだけでなく、読み書き用のkeyもきちんと隠蔽すべきです。
SharedPreferencesはContextから取得するたびに新規のインスタンスが作られるわけでは無く、内部でインスタンスがキャッシュされ、パスが同一であれば同じインスタンスが返るようになっています。つまり、同一パスに対する複数箇所からの読み書きの整合性のために別途Singletonなどで管理する必要はありません。一方、Singleton等でインスタンスを持ち続けてもメモリの逼迫具合は変わりませんので、Singletonにした方が都合がよいのであれば、保持しても問題ありません。
絶対に書き込んでおくべき値はあるか?
どのような値を保持すべきかはアプリによって自由に設定すればよいとは思います。
しかし、どのようなアプリであろうとも、設定のバージョン番号は必ず書き込んでおくようにしましょう。今現在は必要なくても将来必ず必要になります。アプリのバージョンではありません(対応づけができるのであればそれでもよいですが)。どのような値をどのように書き込んでいるかという、設定ファイルのバージョン番号です。
読み書きする値が増えたり減ったり、読み替える必要があるタイミングでバージョン番号をインクリメントしておくと、後ほど設定ファイルのマイグレーションを行う際に役に立ちます。
私のアプリでは起動時に以下のような処理を行っており、
設定バージョンを読み出して、現在のバージョンなら何もしない。旧バージョンならクリアするということをしています。
fun maintain(storage: SettingsStorage) {
val currentVersion = getSettingsVersion(storage)
if (currentVersion == SETTINGS_VERSION) {
return
}
storage.clear()
storage.writeInt(Key.SETTINGS_VERSION, SETTINGS_VERSION)
writeDefaultValue(storage, false)
}
少し前までは、ここに旧設定値を新設定値に書きかえる処理を書いていましたが、変更してから十分に時間がたった(1年以上)ため、マイグレーションサポートは打ち切り、設定値をクリアするだけにしています。
バージョン番号だけをみて一定バージョンより古い場合はクリアするという処理が入っているため、それ以前にどのような内容を書き込んでいたかは完全に忘れることができます。
永続化情報は負債となりやすいものですので、それを清算できる仕組みを組み込んでおくことは非常に重要です。
キーをどのように管理すべきか?
SharedPreferencesではStringをキーとして読み書きします。内部では
private Map<String, Object> mMap;
という一つのMapにObjectとして各値が保持されています。
当然、キーの値が重複することは絶対に避けなければなりません。
以下のように定数としてキーを定義する場合があると思います。
const val KEY_HOGE = "hogehoge"
const val KEY_FUGA = "fuga"
この方法は、設定値が数個ならよいですが、数十個以上に増えてくると値が重複しているのかどうかのチェックも難しくなります。さらにキーの定義があちこちに散らばったりするともう手に負えません。
キーが重複してもそれぞれ書き込み読みだしを行うまでは問題が起こりませんので、発見が難しいバグを作ってしまいかねません。
リフレクションを使ったテストを書いておくとか、アノテーションをつけて警告が出るように、などの方法もあるとは思いますが、労力が一番小さな方法としてenumとして定義し、enumの#name()
の値をキーにするのがよいと思います。
enum class Key {
HOGE,
FUGA,
...
}
同じ名前を定義してしまうとそもそもコンパイルが通りませんので、重複が無いことを保証できます。
SharedPreferencesへのアクセスをラップして、引数として必ずこのkeyを指定するようにすれば、新規のキーをどこに追加すべきかということも明確になりますので、定義が散らばってしまって収拾がつかなくなるなんてことも防ぐことができるでしょう。
保存するデータ型が変わったキー
もともと、ある機能のon/offという意味でboolean値で保存していたが、機能が追加され、3択の設定となったため、int値で保存したい。というようなことはよくあることでしょう。
このような場合は、設定値の意味が同じであろうとも、必ずキーの値を変更する必要があります。
旧バージョンではboolean値で書き込んでいるので、それを新バージョンでint値として読み込もうとするとClassCastExceptionが発生してしまいます。
これは旧バージョンで設定値を書き込んだ後、バージョンアップを行った場合にのみ問題が発生するので、テストでは見つけにくい問題です。
リリース直後にクラッシュの嵐という経験をされたかたも少なからずいるのではないでしょうか?
使わなくなったキー
アプリの歴史が長くなれば当然、機能の変更などにより、かつては保存していたが必要なくなった、という設定値が出てくるでしょう。
前項のような型が変わったために名前を変更した場合のそれまで使っていたキーも同じです。
そのような場合に注意しなければならないのが、一度使用したキーは再利用してはならない。ということです。
保存されたファイルのデータはアプリをアップデートしても残り続けますので、旧バージョンで保存されたそのままのファイルを、新バージョンでも読み出せなくてはいけません。
そのとき、同じキーを使ってしまったら・・・
データ型が違っていればClassCastExceptionが発生します。
データ型が同じだった場合は、ぱっと見は動いて見えるけど、初期設定が想定したものと違う、という非常に微妙な不具合になったり、想定していない範囲の値が読めてしまったがためのクラッシュなども発生するでしょう。
当時と同じ開発者であっても気づくのは難しいでしょうし、違う開発者が変更したのであれば気づくことは不可能です。かつてその設定値を使っていたバージョンで使用し、その値を書き込んだ後でバージョンアップした場合にしか問題が発生しませんから、おそらくリリースまでのテストで見つけることも非常に難しい。そして、長く使ってくれているユーザーのいる市場では高頻度で問題が発生します。
このような問題に対応するため、前述のようにenumでキーを管理しつつ、使われなくなったキーは削除するのでは無く、残し続けることをルールとしましょう。使わなくなっていることを明示するため、@Deprecated
をつけるなどしておくとよいでしょう。
@Deprecated
をつけた使用しなくなったキーは、設定バージョンを上げ、マイグレーションサポートを切り捨てられた時にやっと削除できるようになります。
デフォルト値の管理はどうすればいいか?
SharedPreferencesからの読み出しでは以下のように必ずデフォルト値を指定します。値が書き込まれていない場合はこの値が返ります。
sharedPreferences.getInt(key.name, defaultValue)
このデフォルト値をどのように管理すべきかという問題があります。
SharedPreferencesをラップするクラスをもうけて、読み書きメソッドでデフォルト値を書いておくというのもありですが、実コード上にデフォルト値が分散するため、数が多くなると見通しが悪くなってしまいます。
キーの管理で説明したように、キーをenumで定義しているのであれば、キーのenumにデフォルト値を持たせてしまうのがよいと思います。
こんな感じ、使用する設定のキーとそのデフォルト値をまとめて書いておくことができます。
enum class Key {
HOGE(true),
...
}
こうすると、読み出しのメソッドは、Keyを指定するだけで値が無いときにそのキーに応じた適切なデフォルト値を返せるようになります。
fun readBoolean(key: Key): Boolean {
return preferences.getBoolean(key.name, key.getDefaultBoolean())
}
欠点は、読み出す場所によってデフォルト値を変えたい場合でしょうが、そんなことが必要な場合はロジックを見直した方がいいかもしれません。
初期値はリソース管理しているので初期値の取得にContextが必要、となると適用できませんね。
なお、PreferenceFragmentなどを使う場合、xmlにデフォルト値を書くことができ、PreferenceFragmentで読み込んだときか、PreferenceManager#setDefaultValues で呼び出したときに、値が書かれていなければそのデフォルト値が書き込まれる仕組みになっています。
これを利用するという手も無くはないですが、初期値の管理と仕方としては余計な情報が多いこと、汎用的に使えないこと、PreferenceManager#setDefaultValuesではPreferenceScreen等が作られており、実行コストが高い、またPreferenceFragmentCompatを使っている場合はHandlerスレッド以外からのコールでエラーになる、などなど問題があるため、enum管理のものを書き込む独自の仕組みを作った方がよいと思います。
初期値を書き込んでおくべきか?
SharedPreferencesの読み出しメソッドに指定するデフォルト値は、値が書き込まれていない場合の値となります。
PreferenceFragmentなどを利用している場合は画面をinflateした時にxmlに記述したデフォルト値が書き込まれますが、そうでなければ、設定を変更したときにのみその値が書き込まれているという状態になります。
保存すべき値すべてを書き込んでおく必要が無いため効率的です。また、「値が書き込まれていない」という状態を利用することもできるため、あらかじめすべてを書き込むよりも情報量が多く、書き込むデータ量を削減することもできるでしょう。
ただし、あらかじめ値が書き込まれていないと、バージョンアップ時にデフォルト値を変更したいときに問題がでてきます。
読み出しのデフォルト値を変更すると、過去バージョンで設定変更を一度も行っていない場合、動作が変化します。つまり、意図的に設定を変更していないユーザはバージョンアップに伴って設定が勝手に書き換わるという動作になります。
すでにその設定で使い始めているユーザーはそのままで、新規ユーザーのデフォルト値のみを変化させたい場合があります。そうなった場合、設定バージョンを変更し、旧バージョンの場合は旧デフォルト値を書き込んでおくようにする、などの対応が必要になってきます。しかし、はじめからデフォルト値を全部書き込んでおけばこのようなマイグレーション処理は必要なくなります。
これはどちらも一長一短なのでどちらがよいとは言えないと思いますが、デフォルト値を変更する際、すでに使い始めているユーザーをどうするかという観点は抜け落ち安いため、特に多くの開発メンバーが関わるようなプロジェクトであれば、書き出しておいた方がトラブルが少ないと思います。
値の型をどのように管理すべきか?
SharedPreferencesへの読み書きのキーはStringで共通ですが、そのキーに紐付く値の型は統一しておく必要があります。
一カ所でも間違っていると ClassCastException が発生してしまうのですが、必ずしも間違った箇所で発生するのでは無く、後で本来の型でアクセスしたときに発生するというものなので、単純なユニットテストでは見つけにくいという問題があります。
これを適切に管理するにはどうすればいいでしょう?
ここについてはどうすればいいかを模索している最中です。
①enum名に型名を含める
enum class Key {
HOGE_INT,
HOGE_BOOLEAN,
}
一度やったのち、これはないなと思ったのだが、一周回ってこれが一番いいのではとなっている案。
人間の目で見たときに多少わかりやすくする方法です。
ルールが徹底されていれば、異なる型に同じKeyを使ってしまうこともありません。
ただ、こんな風に書いても、コンパイラはエラーを吐いてくれません。
preference.writeInt(Key.HOGE_BOOLEAN, hoge)
Typo等があってそのままリリースしちゃったりしたら修正できないですし、人間の目でチェックする必要があるので限界があります。
以下のようなチェックメソッドを用意して
private const val SUFFIX_BOOLEAN = "_BOOLEAN"
private const val SUFFIX_INT = "_INT"
private const val SUFFIX_LONG = "_LONG"
private const val SUFFIX_FLOAT = "_FLOAT"
private const val SUFFIX_STRING = "_STRING"
private val SUFFIXES =
listOf(SUFFIX_BOOLEAN, SUFFIX_INT, SUFFIX_LONG, SUFFIX_FLOAT, SUFFIX_STRING)
internal fun Array<out Enum<*>>.checkSuffix() {
forEach { key ->
require(SUFFIXES.any { key.name.endsWith(it) }) { "$key has no type suffix." }
}
}
internal fun Enum<*>.checkSuffix(value: Any) {
when (value) {
is Boolean -> require(name.endsWith(SUFFIX_BOOLEAN)) {
"$this is used for Boolean, suffix \"$SUFFIX_BOOLEAN\" is required."
}
is Int -> require(name.endsWith(SUFFIX_INT)) {
"$this is used for Int, suffix \"$SUFFIX_INT\" is required."
}
is Long -> require(name.endsWith(SUFFIX_LONG)) {
"$this is used for Long, suffix \"$SUFFIX_LONG\" is required."
}
is Float -> require(name.endsWith(SUFFIX_FLOAT)) {
"$this is used for Float, suffix \"$SUFFIX_FLOAT\" is required."
}
is String -> require(name.endsWith(SUFFIX_STRING)) {
"$this is used for String, suffix \"$SUFFIX_STRING\" is required."
}
}
}
デバッグ実行時は適切なSuffixがついているかをチェックするようにするとかで、ミスを防ぐことはできるでしょう。
fun writeBoolean(key: K, value: Boolean) {
if (BuildConfig.DEBUG) key.checkSuffix(value)
sharedPreferences.edit().putBoolean(key.name, value).apply()
}
あくまで実行時例外なので、ユニットテストなどを書いていないと抜けがでる可能性はありますが、開発中に一切動作テストをしないでリリースまで進む、なんてことは滅多にないでしょうし、コンパイルエラーになるほど強力な方法でないにしても、実運用上問題が出る可能性が十分低い方法なのではないかと思います。
カスタムLintで警告を出すこともできそうですね。
さらに言えば、この引数にEnumを渡す部分は、パッケージプライベートやモジュールに分けてinternalなどにして、アプリ全体から直接アクセスできないようにしておいた方がよいでしょう。
fun getHoge(): Boolean =
pref.readBoolean(Key.HOGE, false)
fun setHoge(value: Boolean) =
pref.writeBoolean(Key.HOGE, value)
こうしておけば、このクラスの全メソッドに対するシンプルなユニットテストを書くだけで間違いが無いことが担保できます。
②型ごとに別のenumを定義する
enum class IntKey {
HOGE,
...
}
enum class LongKey {
FUGA,
...
}
void putInt(IntKey key, int value) {
...
}
void putLong(LongKey key, long value) {
...
}
型ごとにキーのenumを分け、読み書きをラップするクラスの引数にそのenumのみを許すようにする方法です。アクセスのルールを守る限りは型を間違うと必ずコンパイルエラーになるため、問題が起こりにくくなります。
enumを分けて定義するので、別々のenumに同じ名前の要素を定義できてしまう問題がでてきます。キーとして使うときはプレフィックスなどをつけるなどで簡単に回避できるとは言え、やや気持ち悪さが残ります。
また、たかだか6種類とはいえ、それだけのenumを作る必要がありますし、型ごとに別々のenumにキーを定義するため、関連したキーをまとめて書いておくことができなくなります。
さらに複数のSharedPreferencesを使う場合、一つのenumで複数のSharedPreferences用のキーを定義するのはどこで使うかが曖昧になるのでやめた方がいいでしょうから、そのバリエーション×型の数のenumが必要になります。
強制力は強いものの、ルールが複雑でコストがかかりすぎている感があります。
③enumの初期化時にデフォルト値を指定し、その型以外では使えないようにする
enum class Key {
HOGE(0),
;
private enum class Type {
BOOLEAN,
INT,
LONG,
STRING,
STRING_SET,
}
private val type: Type
private val defaultValue: Any
constructor(value: Any) {
type = when (value) {
is Boolean -> Type.BOOLEAN
is Int -> Type.INT
is Long -> Type.LONG
is String -> Type.STRING
is Set<*> -> Type.STRING_SET
else -> throw IllegalArgumentException()
}
defaultValue = value
}
internal fun isBooleanKey(): Boolean {
return type === Type.BOOLEAN
}
...
}
fun writeBoolean(key: Key, value: Boolean) {
if (!key.isBooleanKey()) {
throw IllegalArgumentException(key.name + " is not key for Boolean")
}
...
}
初期値を必ずenumと併せて定義するようにして、そのときの型以外でのアクセスができなくします。
初期値をenumに持たせるのであれば定義に関するコストは変わりませんし、②の案のように定義がばらついたりむやみにenumが増えたりしません。
ただ、間違った使い方はコンパイルエラーではなく実行時例外になるため②の案よりは弱いです。また、指定したデフォルト値から型の判定を行うため、特に直接リテラルを記述する場合、int``long
の取り違えが起こりやすくなり、間違いに気づきにくいというデメリットがあるかと思います。(L
の付け忘れによるlongのint化、桁あふれによるintのlong化など)
いずれも、簡単なユニットテストで見つけられる範疇ではあります。
④enumの初期化引数に型を含める
public enum Key {
HOGE(Long.class, 0L),
;
@Nullable
private final Class<?> mType;
@Nullable
private final Object mDefaultValue;
<T> Key(@NonNull final Class<T> type, @NonNull final T defaultValue) {
mType = type;
mDefaultValue = defaultValue;
}
③とどっちがいいかと迷っている案、初期化引数として値の型を指定するようにしたものです。デフォルト値との型の不一致はコンパイルエラーになります。
例えば、上記の0L
のL
を忘れるとコンパイルエラーになるため、3のintとlongの取り違えは起こりにくくなります、
しかし、そのためだけに全部に型を指定する必要があるのでちょっと面倒です。
また、③の案と同様、読み書きの段階での間違いは実行時例外になります。
③との違いは、パスワードを二回入力させるような手間をかけることでミスを起こしにくくすると言うものなので、コストに見合うメリットがあるのかと言われれば正直微妙なところ。
あと、これのkotlinでの表現方法がまだ見つけられていないのでここだけJavaです。できるのかな?
情報の永続化はトラブルが起きやすいので工夫が必要です。
今回紹介した方法も、まだまだ改善の余地のあるもの、もしくは根本的な問題をはらんだものかもしれません。
ツッコミどころも多いとは思いますので、ご指摘等いただければ幸いです。
以上です。