中規模以上のアプリでSharedPreferencesをエレガントに扱う

# SharedPreferencesとは

SharedPreferencesはXMLのファイルをアプリ内でシングルトンのインスタンスを通して値を読み書きする機能を提供するクラスです。
アプリでDBを使うほどでもないデータやフラグなどを保存するのに使ったりすると思います。

### SharedPreferencesの生成

SharedPreferenceは保存するファイルの名前とファイルの作成モードを指定してContextから取得します。

```java
context.getSharedPreference(name, mode);
```

modeには以下の種類があります。

生成モード | 説明
:---|:---
MODE_PRIVATE | デフォルトのモード。ファイルを作成したアプリのみがアクセスすることができる。
MODE_WORLD_READABLE | すべてのアプリからファイルを読むことができる。非常に危険なため非推奨。
MODE_WORLD_WRITABLE | すべてのアプリから読み書きすることができる。非常に危険なため非推奨。

このようになっており、今ではアプリ間でデータを共有したい場合はContentProviderを使うことが推奨されています。
また、直接nameとmodeを指定する代わりにPreferenceManagerを使う方法もあります。

```java
PreferenceManager.getDefaultSharedPreferences(context);
```

この方法は、以下のコードと同じになります。

```java
String name = context.getPackageName() + "_preferences";
int mode = Context.MODE_PRIVATE;
context.getSharedPreferences(name, mode);
```

つまりデフォルトではパッケージ名が `com.example.app` だったら `com.example.app_preferences.xml` という名前でファイルが作られることになります。

たとえばそのファイルに `user_id` を保存したら以下のようになります。

```xml
# cat /data/data/com.example.app/shared_prefs/com.example.app_preferences.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="user_id">123</string>
</map>
```

### SharedPreferencesの読み書き

SharedPreferencesのデータにアクセスするために以下のメソッドがあります。

```java
prefs.getBoolean(key, defValue);
prefs.getFloat(key, defValue);
prefs.getInt(key, defValue);
prefs.getLong(key, defValue);
prefs.getString(key, defValue);
prefs.getStringSet(key, defValue);
```

データの更新はEditorを通して行います。

```java
SharedPreferences.Editor editor = prefs.edit();

editor.putBoolean(key, value);
editor.putFloat(key, value);
editor.putInt(key, value);
editor.putLong(key, value);
editor.putString(key, value);
editor.putStringSet(key, value);

editor.commit(); // 同期的に書き込む。書き込みが成功したかを表すbooleanを返す。
editor.apply(); // 非同期で書き込む。結果を受け取ることはできない。
```

# SharedPreferencesにまつわる問題

SharedPreferencesはContextがあればアプリのどこからでも読み書きが可能です。しかし、そのまま扱おうとすると、どこから、どのキーで、どんな型のデータが書き込まれるか分からずに、新しくデータを追加しようとするたびにプロジェクト内でgrepして型を調べて上書きしても大丈夫か調べるということをしなればなりません。長期に渡ってメンテナンスをするのは難しくなりそうですね。
なので一般的にはSharedPreferencesにアクセスを行うクラスを作ったりすると思います。

上で述べたことを踏まえると基本的なデータの読み書き（put, get, has, remove）を行おうとすると以下のようになります。

```java
public class MyPreferenceManager {
    ...

    private static final String KEY_USER_ID = "user_id";

    public putUserId(int userId) {
        prefs.edit().putInt(KEY_USER_ID, userId).apply();
    }

    public int getUserId() {
        return prefs.getUserId(KEY_USER_ID, -1);
    }

    public int getUserId(int defaultValue) {
        return prefs.getUserId(KEY_USER_ID, defaultValue);
    }

    public boolean hasUserId() {
        return prefs.contains(KEY_USER_ID);
    }

    public void removeUserId() {
        prefs.edit().remove(KEY_USER_ID).apply();
    }
```

ひとまずどこからどんなデータが書き込まれているかは追いやすくなりましたが、1つのキーに対して20行のコードを書く必要があり、冗長に見えます。

私はキーの数が100個近いSharedPreferencesの管理をしたことがありますが、もう少しエレガントな方法はないものかと考えていました。

# KVS Schemaを使う

そんなことがあり、自分でライブラリを作りました。

* [rejasupotaro/kvs-schema](https://github.com/rejasupotaro/kvs-schema)

このライブラリは、SharedPreferencesに何が保存されているかというスキーマクラスを書いて、そこから実際にアクセスするクラスを生成するという基本思想になっています。それに加えいくつかの便利な機能も提供しています。

### KVS Schemaの使い方

最初にスキーマクラスを定義します。`@Table(name = "...")` がファイル名になり `@Key(name = "...")` がファイルに保存されるキー名になります。 クラス名は `*Schema` である必要があります。

```java
@Table(name = "example")
public abstract class ExamplePrefsSchema {
    @Key(name = "user_id") int userId;
}
```

KVS Schemaは上のスキーマクラスから `Schema`　サフィックスを取った名前でクラスを生成してたとえば `user_id`　　というキーに対して以下のメソッドが自動生成されます。

```java
ExamplePrefs prefs = ExamplePrefs.get(context);
prefs.putUserId(userId);
prefs.putUserId(userId, defaultValue);
prefs.getUserId();
prefs.hasUserId();
prefs.removeUserId();
```

KVS Schemaは `boolean` `String` `float` `int` `long` `Set<String>` をサポートしています。

スキーマクラスにはファイル名、キー名、型などSharePreferencesの情報が一望できるようになっており、新しくキーを追加するときに一行追加するだけで、必要なメソッドを追加することができます。

以下、応用的な使い方を紹介します。

### デフォルト値を指定する

getメソッドの引数にデフォルト値を指定することで、値が保存されていなかったときにその値を返すことができますが、

```java
prefs.getUserId(defaultValue);
```

KVS Schemaはコンパイル時にスキーマクラスを処理しているので、コンパイル時に定数になる値は読むことができます。

```java
@Table(name = "example")
public abstract class ExamplePrefsSchema {
    @Key(name = "user_id") final int userId = -1;
}
```

それを利用して右辺が定数で指定されたときはgetメソッドの引数にデフォルト値を指定しなくても、スキーマクラスの値を返すことができるようになっています。逆に、コンパイル時に定数にならないものを指定することはできません。

### シリアライザーを使う

モデルクラスをそのまま保存したいこともあるかと思います。その場合は `@Key(serializer = *.class)` でシリアライザーを指定することができます。

```java
// スキーマクラス
@Table(name = "example")
public abstract class ExamplePrefsSchema {
    @Key(name = "user", serializer = UserPrefsSerializer.class) User user;
}

// シリアライザークラス
public class UserSerializer implements PrefsSerializer<User, String> {
    @Override
    public String serialize(User src) {
        return GSON.toJson(src);
    }

    @Override
    public User deserialize(String src) {
        return GSON.fromJson(src, User.class);
    }
}

// 使い方
prefs.putUser(user);
```

`PrefsSerializer<A, B>` の `A` が元のモデルの型として扱われ `B` がシリアライズ後の型になり `B` はキーの右辺と一致している必要があります。
モデルを保存するときはバージョンアップで構造が変わってクラッシュしないか気を付ける必要があります。

### ビルダーを使う

SharedPreferencesの生成モードを変えたい、サードパーティの暗号化機能が入ったSharedPreferencesを使いたいなど、SharedPreferencesをカスタムしたいことがあると思います。そのようなときのために `@Table(builder = *.class)` でSharedPreferencesを生成するビルダークラスを指定することができます。

```java
// スキーマクラス
@Table(name = "example", builder = ExamplePrefsBuilder.class)
public abstract class ExamplePrefsSchema {
    @Key(name = "user_id") int userId;
}

// ビルダークラス
public class ExamplePrefsBuilder implements PrefsBuilder<ExamplePrefs>　{
    @Override
    public ExamplePrefs build(Context context) {
        ...
        return new ExamplePrefs(...); // 任意のSharedPreferencesを渡すことができる。
    }
}
```

### Kotlinから使う

KVS Schemaにはputメソッドのエイリアスとしてsetメソッドが存在していますが、これはKotlinのプロパティシンタックスを使うためです。

```kotlin
prefs.userId = "Kotlin"
prefs.userId // => Kotlin
```

### 既存のアプリにKVS Schemaを導入する

KVS　SchemaはSharedPreferencesのマッピングに過ぎないので、既存のアプリからでも比較的簡単にマイグレーションすることができます。マイグレーションの補助のために `SharedPreferencesInfo` というクラスを使って実際にSharedPreferencesに保存されているデータを出力して確認することもできます。

```java
for (SharedPreferencesTable table : SharedPreferencesInfo.getAll(this)) {
    Log.d("DEBUG", table.toString());
}
```

```
   name: com.example.android.kvs_preferences
   path: /data/data/com.example.android.kvs/shared_prefs/com.example.android.kvs_preferences.xml
 ╔═══════════╤══════════════╤════════╗
 ║ Key       │ Value        │ Type   ║
 ╠═══════════╪══════════════╪════════╣
 ║ user_name │ Smith        │ String ║
 ╟───────────┼──────────────┼────────╢
 ║ user_id   │ 1            │ String ║
 ╚═══════════╧══════════════╧════════╝
```

具体例: [Introduce kvs-schema by rejasupotaro · Pull Request #311 · konifar/droidkaigi2016](https://github.com/konifar/droidkaigi2016/pull/311)

# まとめ

SharedPreferencesの使い方と問題点、それの一つの解決方法としてKVS Schemaの説明をしました。KVS Schemaの最初のコミットが2014年で、2016年現在でもために機能追加して、最近バージョン5.0に上げたので、自分の中では結構メンテが続いているなあと思います。
機能要望や疑問があればissueを立ててもらえれば協力できると思います。