KotlinでSharedPreferencesのラッパーを作った

# 概要

AndroidのSharedPreferencesを利用するためのラッパーライブラリ「Prefs」を作りました。Kotlinの利用を想定していますが、Javaでも使えます。
AAR形式のライブラリとなります。

**ソースコード**

wa2c/prefs: Android SharedPreferences wrapper library
https://github.com/wa2c/prefs

# 詳細

ライブラリの詳細については、[README-ja.md](https://github.com/wa2c/prefs/blob/master/README-ja.md)からの抜粋となります。

## 機能

### 対応データ型

操作可能なデータ型を追加します。全ての型に、値を取得するgetメソッドと、値を保存するputメソッドがあります。データはSharedPreferencesの型に変換されて処理されます。

対応する型と、get/putメソッドおよび、実際に保存される際の型は次の通りとなります。

| データ型      | get             | put             | 実際の型      | 変換処理                    |
|---------------|-----------------|-----------------|---------------|-----------------------------|
| Boolean       | getBoolean      | putBoolean      | Boolean       |                             |
| Byte          | getByte         | putByte         | Int           |                             |
| Short         | getShort        | putShort        | Int           |                             |
| Int           | getInt          | putInt          | Int           |                             |
| Long          | getLong         | putLong         | Long          |                             |
| Float         | getFloat        | putFloat        | Float         |                             |
| Double        | getDouble       | putDouble       | Long          | doubleToRawLongBits         |
| BigInteger    | getBigInteger   | putBigInteger   | String        | toString                    |
| BigDecimal    | getBigDecimal   | putBigDecimal   | String        | toPlainString               |
| Char          | getChar         | putChar         | String        | toString                    |
| String        | getString       | putString       | String        |                             |
| Set\<String\> | getStringSet    | putStringSet    | Set\<String\> |                             |
| ByteArray     | getBin          | putBin          | String        | Base64                      |
| Serializable  | getSerializable | putSerializable | String        | ObjectOutputStream + Base64 |
| Object        | getObject       | putObject       | String        | Gson                        |

SerializableおよびObjectについては、正しくget/putできるかは処理対象クラスの実装に依存します。また、proguard等により難読化された場合、正しく処理することができません。


### 初期化

以下のようにインスタンスを初期化します。名前を省略すると、デフォルトのSharedPreferencesが使用されます

~~~kotlin
// デフォルト初期化
var prefs1 = Prefs(context)
// 名前付き初期化
var prefs2 = Prefs(context, "name")
~~~

### putメソッド

putメソッドは次のように行います。edit()およびapply()は不要です。メソッドチェーンの記述に対応しています。

~~~kotlin
// 通常
prefs.putInt("pref_1", 123)
// メソッドチェーン
prefs
    .putString("pref_2", "abc")
    .putString("pref_3", "def")
~~~

複数の値を一度にputする場合、begin()およびend()を使用します。

~~~kotlin
prefs
    .begin()
    .putBoolean("val1", true)
    .putInt("val2", 1)
    .putLong("val3", 1L)
    .putFloat("val4", 1.0f)
    .putString("val5", "1")
    .end()
~~~


### getメソッドのNon-null型, Nullable型対応

すべての型に、Non-null型を返すgetメソッドと、Nullable型を返すgetメソッドを使用できます。Nullable型を返すメソッドは、名前の末尾に"OrNull"が付きます。

~~~kotlin
// Int?
val v1 = prefs.getIntOrNull("pref_v1")
// Int
val v2 = prefs.getInt("pref_v2", 0)
// String?
val v3 = prefs.getStringOrNull("prefs_v3")
// String
val v4 = prefs.getString("prefs_v4", "")
~~~

### getメソッドのデフォルト値省略

Serializable、Objectを除き、Non-null型を返すgetメソッドはデフォルト値の省略が可能です。

~~~kotlin
// 省略記述
val v1 = prefs.getInt("pref_v1")
// 上と同じ
val v2 = prefs.getInt("pref_v2", 0)
// 省略記述
val v3 = prefs.getString("prefs_v3")
// 上と同じ
val v4 = prefs.getString("prefs_v4", "")
~~~

デフォルト値はプロパティとして以下のように定義されており、変更可能です。

~~~kotlin
var defaultBooleanValue : Boolean = false
var defaultByteValue : Byte = 0
var defaultShortValue : Short = 0
var defaultIntValue : Int = 0
var defaultLongValue : Long = 0
var defaultFloatValue : Float = 0.0F
var defaultDoubleValue : Double = 0.0
var defaultBigIntegerValue : BigInteger = BigInteger.ZERO
var defaultBigDecimalValue : BigDecimal = BigDecimal.ZERO
var defaultCharValue : Char = '\u0000'
var defaultStringValue : String = ""
var defaultStringSetValue : Set<String> = HashSet(0)
var defaultBinValue : ByteArray = ByteArray(0)
~~~

### リソースIDの使用

getおよびputの対象キーに、文字列リソースIDを使用することができます。

~~~kotlin
// get - 文字列指定
val v1 = prefs.getIntOrNull("pref_v1") 
// get - リソースID指定
val v2 = prefs.getIntOrNull(R.string.pref_v2)
// put - 文字列指定
prefs.putInt("pref_v3", 123) 
// put - リソースID指定
prefs.putInt(R.string.pref_v4, 456)
~~~

Serializable、Object型を除き、Non-null型を返すgetメソッドのデフォルト値に、リソースIDを使用することができます。
第3引数がデフォルト値リソースIDの指定となります。リソースIDを指定した場合、第2引数のデフォルト値は無視されます。

~~~kotlin
// 第3引数指定 (第2引数は無視)
val v1 = prefs.getInt(R.string.pref_v1, -1, R.string.v1)
// 名前付き引数指定
val v2 = prefs.getInt(R.string.pref_v2, defRes = R.integer.v2) 
~~~

利用可能なリソースの型は、データ型で異なります。利用可能なリソースは次の通りです。

| データ型      | bool | integer | float | dimen | string | array-string | raw, etc. |
|:--------------|:----:|:-------:|:-----:|:-----:|:------:|:------------:|:---------:|
| Boolean       |   *  |    *    |   *   |   *   |    *   |       -      |     -     |
| Byte          |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| Short         |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| Int           |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| Long          |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| Float         |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| Double        |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| BigInteger    |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| BigDecimal    |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| Char          |   -  |    *    |   *   |   *   |    *   |       -      |     -     |
| String        |   *  |    *    |   *   |   *   |    *   |       -      |     -     |
| Set\<String\> |   -  |    -    |   -   |   -   |    -   |       *      |     -     |
| ByteArray     |   -  |    -    |   -   |   -   |    -   |       -      |     *     |

### 型変換

SerializableおよびObjectのgetメソッドにおいて、型推論を利用することができます(明示的に型パラメータを与えることもできます)。Javaから利用することはできません。

~~~kotlin
// 型推論
var v1: SampleObject? = pref.getObjectOrNull("pref_v1")
// 型パラメータ
var v2 = prefs.getObjectOrNull<SampleObject?>("pref_v2")
~~~

Kotlin以外から利用する場合は、引数にClassまたはType(Objectのみ)を与えることで、戻り値の型変換を行うことができます。

~~~kotlin
// Class
var v3 = prefs.getObjectOrNull("pref_v3", ObjectTestData::class.java)
// Type
var v4 = prefs.getObjectOrNull("pref_v4", object: TypeToken<ObjectTestData>(){}.type)
~~~

### インデックス演算子 (Kotlinのみ) *[2018-11-15 追記]*

インデックス演算子により、get/putメソッドを省略することができます。getはNullable型を返します。型推論により適用メソッドが自動的に決定されます。

~~~kotlin
val v1 = 123
prefs["pref_v1"] = v1 // putInt
val v1_ : Int? = prefs["pref_v1"] // getIntOrNull

val v2 = "abc"
prefs[R.string.pref_v2] = v2 // putString
val v2_ : String? = prefs[R.string.pref_v2] // getStringOrNull
~~~

## 動作環境

- Android API Level 14 以降

## インストール

次の記述をbuild.gradleファイルに追加してください。

```gradle
    repositories {
        maven { url 'https://github.com/wa2c/prefs/raw/master/repository/' }
    }

    dependencies {
        implementation 'com.wa2c.android:prefs:0.1.1'
    }
```

## サンプル

サンプルアプリおよびテストコードを参照してください。

* [サンプルアプリ](https://github.com/wa2c/prefs)
* [Kotlinテスト](https://github.com/wa2c/prefs/blob/master/app/src/test/java/com/wa2c/android/prefsapp/PrefsKotlinUnitTest.kt)
* [Javaテスト](https://github.com/wa2c/prefs/blob/master/app/src/test/java/com/wa2c/android/prefsapp/PrefsJavaUnitTest.java)

## ライセンス

[MIT](hhttps://github.com/wa2c/prefs/blob/master/LICENSE.txt)



# 作成した経緯

Androidのデータ永続化に用いられるSharedPreferencesは、お手軽にデータを永続化する上で便利なものですが、個人的には次のような不満がありました。

## 不満1. 呼び出しが面倒

```kotlin
// デフォルト
var p1 = PreferencesManager.getDefaultPreferences(context);
// 名前付き
var p2 = context.getSharedPreferences("name", Context.MODE_PRIVATE)
```
これを一々書くのがダルいですし、今はContext.MODE_MULTI_PROCESSが非推奨になったので、Context.MODE_PRIVATE一択のはず。なので、名前付きの第2引数は不要のはず。そもそもの話、SharedPreferencesとか、PreferencesManagerという名前そのものが長いです。

余談ですが、ものすごく個人的な経緯として、名前が長いので開発中にローカルな変数の名前がその時の気分やスコープに応じて、「sharedPreferences」とか「preferences」とか「prefs」とかバラバラになってしまいやすいので、最初から名前その物を短くしたかったというのがあります。


### 変更ポイント

- 呼び出しを簡素化した
- クラスの名前を短くした

## 不満2. 扱える型が少ない

標準で保存できる型が、boolean, int, float, long, String, Set\<String\>です。この面子の中にSet\<String\>が入ってるのも微妙な感じです。経緯はよく分からないのですが、MultiSelectListPreferenceから設定した値の操作を考慮したものなのでしょうか（多分）。Bundleに格納できるデータ型と共通化してくれれば、色々便利だったと思うのですが。せめて、各種プリミティブ型やシリアライズ可能な型ぐらいは保存できてほしかった。（もしかしたら、本来の目的とは異なるのかもしれませんが。）


### 変更ポイント

- 保存できる型を増やした

## 不満3. getの初期値指定がダルい

値を読み込む時の各種getメソッドは、未保存の際に値を返すための初期値を指定します。これは戻り値がnullにならないことを保証するための設計なのでしょう（多分）。ただ、一々初期値を指定するの面倒ですし、特に値が無ければ無いでnullなりデフォルト値を返してほしい場面は多々あるはずです。特に、Kotlinはnullable/non-nullの型がありますので、ユーザが自由に選択できれば何かと便利だと思います。

### 変更ポイント

- デフォルトの初期値の指定を省略可能とした
- 戻り値のnullable型、non-null型を返すメソッドを分けた

## 不満4. キーをリソースIDで受け付けてくれない

アプリによっては、設定のキーを文字列リソースに定義することがあります。特に、PreferenceActivity/PreferenceFragmentを使って設定画面を構築する場合に、設定画面を定義するXMLで、キーに文字列リソース参照(@string/\*)を設定する場合があります。
アプリ側の実装で該当の設定値を操作するためには、同じ文字列リソースをgetString(R.string.\*)で参照する必要がありますが、数が多いと結構面倒です。そこで、キーの指定に文字列だけでなくリソースIDを受け付けてくれれば、多少なりとも記述が楽になるはずです。

### 変更ポイント

- キーの指定を文字列リソースIDでも可能とした

例を次に示します。

~~~xml:pref.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
        <CheckBoxPreference
            android:key="@string/pref_check1"
            android:summary="@string/pref_check1_summary"
            android:title="@string/pref_check1_title" />
</PreferenceScreen>
~~~

~~~kotlin
val check1 = prefs.getBoolean(R.string.pref_check1)
~~~

## 不満5. デフォルト値をリソースIDで受け付けてくれない

キーと同様に、PreferenceActivity/PreferenceFragmentを使う際、デフォルト値をリソースで定義し、XMLから参照することがあります。ただ現状、キーと初期値をリソースで定義した上で、コード上でSharedPreferenceから値を取得しようとした場合、以下のような長い記述になってしまいます。（Activity内での利用を想定）

~~~kotlin
val val1 = prefs.getString(getString(R.string.pref_val1), getString(R.string.pref_val1_default));
~~~

正直、書きたくないです。なので、デフォルト値は直に書いてしまうことが多いのですが、初期値ズレで悩まされたことも多々あり、できれば回避したいところです。
そこで、デフォルト値にリソースIDを使う事で、多少記述を簡略化したいところです。
加えて、EditTextPreferenceなど一部のPreferences要素では、初期値を\<integer\>でも\<string\>でも\<dimen\>でも受け付けてくれたり、柔軟な定義が可能となっていますので、それに合わせてある程度型を柔軟に吸収できるようにできると楽かと思います。

### 変更ポイント

- 初期値の引数にリソースIDを使用できるようにした
- 初期値のリソース型はある程度柔軟に対応できるようにした

例を次に示します。

~~~xml:pref.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
        <EditTextPreference
            android:defaultValue="@integer/pref_text1_default"
            android:key="@string/pref_text1"
            android:summary="@string/pref_text1_summary"
            android:title="@string/prev_text1_title" />
</PreferenceScreen>
~~~

~~~kotlin
val text1 = prefs.getString(R.string.pref_text1, R.integer.pref_text1_default)
~~~

## 不満6. 書き込み時のapplyが面倒

値の書き込みには、edit()で始まり、apply()で終える必要があります。しかし、一々書くのが面倒なので省略できれば楽だと思います。（余談ですが、最近はIDEが警告してくれるので書き漏らしは無くなりましたが、昔commit()/apply()漏れをよくやってたような気がします）
加えて、[以前調査した結果](https://qiita.com/wa2c/items/98aa3593558f5355aaa4)によると、複数の値を書き込む場合はまとめてputメソッドを呼んで、最後にapply()を実行した方が圧倒的に処理が高速になるため、こちらの方法も任意で採用できるようにしたいところです。

### 変更ポイント

- applyしなくても値を保存するようにした
- 復数の値を一括で書き込む方法も使えるようにした
- インデックス演算子による操作をできるようにした *[2018-11-15 追記]*


# まとめ

そんな感じで、自分が日頃SharedPreferencesを扱う際に不満に感じていた部分を色々改善したかったため、このようなラッパーを作成しました。これは実際には、個人開発アプリ内で以前から使ってたものをライブラリ化したものです。

ライブラリ自体は、ファイル1つの単純なもので、そんなに複雑な処理もしておらず、薄いラッパーになっているので、サイズもそれほど大きくはなく、使い勝手もあえて現状とあまり変わらないようにしたため、導入自体にさほど大きな障害はないと思います。

実行パフォーマンスについてはあまり考慮していないので、速度を求める場合は別の手段を考えた方が良いと思います。また、Byte配列の保存は最大サイズなどの検証はしていないので、あまり大きなバイナリを入れるのは止めた方がいいでしょう。小規模なデータに使うことを想定しています。

このライブラリで少し微妙な部分は、デフォルト値をリソースID指定する際、第3引数で指定する辺りかと思います。これは、第2引数だとInt型の扱いに困ってしまうので、やむなくこうした設計にしています。もう少し良い方法が思いつくと良かったのですが…。

それから、インデックス演算子のget/putですが、思いついて作ってはみましたが、型が分かりにくくなるのでイマイチな感じがします。putしてみたけど、実際に想定する型と異なる型で保存されていた、なんてことになりそうなので、作った人間が言うのもアレですが、あまり推奨しません。 *[2018-11-15 追記]*

余談ですが、私がAndroidの開発を始めた頃、設定画面とプログラムのキーと初期値を一致させようとした時、「こんなに記述が面倒なのか！」と愕然とした記憶がありますが、何年も経った今になっても、一向に改善される兆しがありません（改善されないという事は、必要としている人はそう多くはないのかもしれませんが…）。
なので、Kotlinのリリースに合わせて自分で作成してしてしまったという経緯があります。Kotlinは、引数の省略やNullable/Non-null型の扱いなど、何かと便利な要素があったので、作る上でとても都合が良かったです。
ただそれでも未だに、本家がもう少し簡単に設定を保存できる仕組み（加えて、設定画面とうまく整合性をとる仕組み）を提供してくれればなぁ、と思わずにはいられません。