GarumとSharedPreferencesへの異常なまでの愛


# はじめに

こんにちは [@operandoOS](https://twitter.com/operandoOS) です。

この記事は [Android Advent Calendar 2014](http://qiita.com/advent-calendar/2014/android) の11日目の記事です。

今回はAnnotation styleでSharedPreferencesが扱える**[Garum](https://github.com/operando/Garum)**というライブラリを作ってるので、その話をしたいと思います。

**無駄話が長すぎるので、Garumの説明から読みたい方は、以下をクリックしてください！**

**[Let's go Garum](http://qiita.com/operandoOS/items/8af20ac09a9d6acb075e#garum%E3%81%AE%E7%B4%B9%E4%BB%8B---annotation-style-sharedpreferences)**


# [SharedPreferences](http://developer.android.com/reference/android/content/SharedPreferences.html)のおさらい

もうみなさんご存知かと思いますが、[SharedPreferences](http://developer.android.com/reference/android/content/SharedPreferences.html)について軽くおさらいします。

SharedPreferencesは、簡単にまとめると以下のような感じです。

* Androidにおける、アプリケーション内でデータ保存する方法の一つ
* アプリケーションのアンインストール、またはデータ削除を行うまで永続的にデータを保存できる
* key-value形式でデータを保存する
* 実データはXML形式で保存されている

使い方については、[API Guides](http://developer.android.com/guide/index.html) -> [Storage Options](http://developer.android.com/guide/topics/data/data-storage.html) -> [Using Shared Preferences](http://developer.android.com/guide/topics/data/data-storage.html#pref) を参照してください。

# SharedPreferencesのつらみ

SharedPreferencesはとても使いやすくて、わかりやすいAPIなのですが、**ちょっとしたつらみ**がいくつかあります。

そのSharedPreferencesのつらみをいくつかあげたいと思います。

**「お前のつらみなんてどうでもいいよ！」**という方は、読み飛ばしてください。

**「端的に述べよ」**という方は、下の表を読んでそれぞれの詳細は読み飛ばしてください。

| 問題 | なぜ辛い？ |
|:-----------|:------------|
| Utilクラスパターン | Utilクラスの実装をもうしたくない。好みが出るので辛い。ほぼわがまま。 |
| Mode問題 | セキュアじゃないModeを使う人がいて辛い。 |
| getDefaultSharedPreferences問題 | 全ての値を一つのSharedPreferencesで管理する。数が増えると辛い。 |
| Keyのベタ書き問題 | Keyの名前を定数化せず、色んなところでベタ書きしてる。変更に弱いので辛い。 |

**※あくまで個人的なつらみ**   

# Utilクラスパターン

SharedPreferencesのUtilクラスでよく見るパターンが２つあります。

* Keyごとのメソッドがあるパターン - アプリ特化型
* Keyを引数で渡すパターン - 汎用型

ちょっと長いですが、この２つのパターンについて説明したいと思います。

## Keyごとのメソッドがあるパターン - アプリ特化型

Keyごとにメソッド(アクセッサー)を作成し、SharedPreferencesの処理は全てUtilクラス内部で操作する実装です。

Keyを意識せず、Utilクラスを使う人にSharedPreferencesをあまり意識させないように設計しています。

例として、Google I/Oのコードである**[iosched](https://github.com/google/iosched)**の**[PrefUtils](https://github.com/google/iosched/blob/master/android/src/main/java/com/google/samples/apps/iosched/util/PrefUtils.java)**を見ると、このパターンの特徴がわかります。

https://github.com/google/iosched/blob/master/android/src/main/java/com/google/samples/apps/iosched/util/PrefUtils.java

このパターンを**「アプリ特化型」**と勝手に読んでます。

Keyの名前はアプリごとに変わります。Keyの名前も数もアプリごとに違うはずです。

このパターンのUtilクラスは、**特定アプリのKeyに依存した実装**になっているため、アプリ特化型と読んでいます。

## Keyを引数で渡すパターン - 汎用型

Keyを引数で渡し、SharedPreferencesの処理は全てUtilクラス内部で操作する実装です。

こちらは先ほどのパターンと違い、使う人にKeyは意識させますがSharedPreferencesはあまり意識させないように設計してます。

例として、みんな大好き[Rebuild](http://rebuild.fm/)のコードである[Rebuild](https://github.com/rejasupotaro/Rebuild)の[PreferenceUtils](https://github.com/rejasupotaro/Rebuild/blob/master/Rebuild/src/main/java/rejasupotaro/rebuild/utils/PreferenceUtils.java)を見ると、このパターンの特徴がわかります。

https://github.com/rejasupotaro/Rebuild/blob/master/Rebuild/src/main/java/rejasupotaro/rebuild/utils/PreferenceUtils.java

ioschedのPrefUtilsとの違いは、Keyごとのメソッドは存在しません。型ごとのメソッドが存在し、そこにKeyを引数で渡す実装になってます。

このパターンを**「汎用型」**と勝手に読んでます。

Keyの名前はアプリごとに変わりますが、SharedPreferencesの処理は変わりません。

変わらない部分の操作をUtil化したパターンというわけです。こちらの方がUtilというには相応しいかもしれません。

このパターンのUtilクラスを実装すると、**どのアプリでも使用できる**Utilクラスが作成できます。


# Mode問題 - ファイル等を作成する際に指定するパーミッション

[getDefaultSharedPreferences](http://developer.android.com/reference/android/preference/PreferenceManager.html#getDefaultSharedPreferences(android.content.Context))メソッドを使用した場合には、常にModeが[MODE_PRIVATE](http://developer.android.com/reference/android/content/Context.html#MODE_PRIVATE)で作成されます。

MODE_PRIVATEで作成したファイルであれば、自身のアプリからしか読み書きできないようになるので問題ありません。

しかし、[getSharedPreferences](http://developer.android.com/reference/android/content/Context.html#getSharedPreferences(java.lang.String, int))メソッドを使った場合には、Modeを自身で指定しないといけません。

この時に、以下のModeを指定してしまうと、**他のアプリから作成したファイルへの読み書きが可能**になってしまいます。

* [MODE_WORLD_READABLE](http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_READABLE)
* [MODE_WORLD_WRITEABLE](http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_WRITEABLE)

ちなみに上記２つのModeは、**API Level 17から非推奨**になっています。

このMode対策の一つとして、**Modeやパーミッションに詳しくない人**でも問題なく使えるようにするため、先ほどの**Utilクラス内でModeをMODE_PRIVATEに固定する**という手法があります。

セキュリティを意識しなくていい！という話ではありませんが、このようにすると対策にはなると思います。


# getDefaultSharedPreferences問題

[getDefaultSharedPreferences](http://developer.android.com/reference/android/preference/PreferenceManager.html#getDefaultSharedPreferences(android.content.Context))メソッドは非常に使いやすいです。

```java
package com.os.operando.sharedpreferences.sample;

// this -> Context
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
```

getDefaultSharedPreferencesメソッドを使用すると、以下のように**「パッケージ名 + _preferences.xml」**というXMLが作成されます。

```
/data/data/[package name]/shared_prefs/com.os.operando.sharedpreferences.sample_preferences.xml
```

[getSharedPreferences](http://developer.android.com/reference/android/content/Context.html#getSharedPreferences(java.lang.String, int))メソッドと比べると、**引数が少ない**ことや**ファイル名を自身で考えなくてもいい**という点があります。

```java
package com.os.operando.sharedpreferences.sample;

SharedPreferences sp = getSharedPreferences("app_pref", MODE_PRIVATE);
// → /data/data/[package name]/shared_prefs/app_pref_preferences.xml
```

しかし、getDefaultSharedPreferencesメソッドを使用することはあまりオススメしません。

状況によりけりですが、オススメしない理由として、以下のようなことがあります。

* 用途ごとにファイルを作成し、そこでそれぞれの値を管理する方がよい
* SharedPreferencesになんでもかんでも値をぶっ込む習慣が生まれる可能性が高くなる
* 値が増えるとI/Oの時間が長くなる。意識するほどではないですが、XMLのパースを最小限に抑える

## 用途ごとにファイルを作成し、そこでそれぞれの値を管理する方がよい

値の管理は、**それぞれの用途ごとにファイルを作成して管理する**方がいいと思ってます。

SharedPreferences内で管理する値が10個未満の場合ならば、getDefaultSharedPreferencesメソッドを使用してもいいかもしれませんが、将来的に増えていく見込みがあるならファイルを分けるべきだと思います。

規模の大きいアプリ等では、SharedPreferencesのファイルが10ファイル以上あったりします。

## SharedPreferencesになんでもかんでも値をぶっ込む習慣が生まれる可能性が高くなる

しかし、気をつけてほしいことが**SharedPreferencesになんでもかんでも値をぶっ込む習慣を作らない**ことです。

**とりあえず色んなところで必要になる値っぽいだから、SharedPreferencesにぶっ込むか！**的な用途で使用する例を一度見たことがあります。

これをやるとファイルを分割してもあまり意味がありません。しかも、意味の分からないKeyが増え続けるので絶望的です。


## 値が増えるとI/Oの時間が長くなる。意識するほどではないですが、XMLのパースを最小限に抑える

SharedPreferences内で管理する値が増えるということは、**実データを保存しているXMLの容量が増える**ということです。

しかし、書いといてなんですがI/Oについては、**一度取得したSharedPreferencesをstaticフィールドで保持する**ような実装？がAPI内部にあったような気がします。

推測になりますがプロセスが消えない限り、一度取得したSharedPreferencesは以後staticフィールドから取得するので、読み込み時にはI/Oが行われないと思ってます。

# Keyのベタ書き問題

SharedPreferencesの値は、**key-value形式で管理**しているので、アプリ内のどこかにKeyを定義する必要があります。

このKeyの定義を**定数で宣言**しないと、以下の例のように**変更に弱いコード**になってしまいます。

```java
SharedPreferences sp = getSharedPreferences("app_pref", MODE_PRIVATE);

sp.getBoolean("key_sample", false);

sp.edit().putBoolean("key_sample", true).commit();
```

Keyの名前である**key_sample**の文字列がベタ書きされてます。

もしKeyの名前を変えることになった場合、二箇所の修正が必要になります。

上記のように二箇所だけならいいですが、他のところでもたくさん使われていた場合に修正箇所が増えてしまします。

Keyの名前を変えるということは稀かもしれませんが、やはりKeyの名前を定数で宣言した方が修正箇所が最小限に抑えられます。

# Garumの紹介 - Annotation style SharedPreferences

SharedPreferencesのつらみをツラツラ述べてきたわけですが、一言言わせていただくと

**「もうSharedPreferencesなんてやってられるか！」**

という心境です。

アプリを新しく作る度に、毎回同じ実装を書くのはもう疲れた。  

**SharedPreferencesをもっと使いやすくしてあげたい！**という気持ちから作ったライブラリが**[Garum](https://github.com/operando/Garum)**です。   

**Garum - https://github.com/operando/Garum**

Garumは、アノテーションを使用してSharedPreferencesのkey-value形式の読み書きを簡単にするものです。

値の書き込みは、アクセッサーやフィールドへの代入で行います。

値の読み込みは、後ほど紹介するModelをインスタンス化した際に自動的に読み込まれる仕組みになっています。

Garumの実装は、使ってみればわかりますが実は[ActiveAndroid](https://github.com/pardom/ActiveAndroid)の実装をパクって作られたものです。

**ActiveAndroid風にSharedPreferencesを使えるライブラリ**と書くと大体どんなコードになるのか、想像できると思います。

# Garumのセットアップ

以下のサイトからjarをダウンロードして、libsディレクトリに入れることで使用できるようになります。 

**http://operando.github.io/Garum/#dowonload**

Mavenにはまだ登録していないので、jarでお願いします！

ちなみに、今の最新バージョンが **0.0.2**でまだまだ試作段階なライブラリです。

**「ふーん。こんなのがあるのね。はいはい」**程度の関心で見ていただければいいかと思います。

# Modelの作成

ここで作成するModelが、SharedPreferencesの一つのXMLになります。

Modelの作成は、以下のことが重要です。

* PrefModelクラスを継承する
* クラスに@Prefアノテーションを指定する
* SharedPreferencesに保存する値(フィールド)には、@PrefKeyaアノテーションを指定する
* フィールド名がKeyの名前になります
* フィールド名とKeyの名前を別にしたい場合には、@PrefKeyアノテーションにKeyの名前を指定する

[@Prefアノテーション](https://github.com/operando/Garum/wiki/@Pref-Annotation)については、以下を参照ください。この記事では割愛させていただきます。

https://github.com/operando/Garum/wiki/@Pref-Annotation

例では、フィールドのクセス修飾子がpublicですが、privateなどでも大丈夫です。

privateにした場合は、[Lombok](http://projectlombok.org/)の@Dataアノテーションを指定して、アクセッサーを自動生成することをオススメします。

```java
@Pref(name = "app_status")
public class AppStatus extends PrefModel {

    @PrefKey("name") // 別の名前をつける。Keyの名前は「name」となる
    public String appName;

    @PrefKey
    public int startupCount;

    @PrefKey
    public boolean showNotification;
}
```
Keyが保存されていなかった際のデフォルト値をフィールドに指定できるアノテーションも用意しています。

* [@DefaultInt](https://github.com/operando/Garum/blob/master/garum/src/main/java/com/os/operando/garum/annotations/DefaultInt.java)
* [@DefaultLong](https://github.com/operando/Garum/blob/master/garum/src/main/java/com/os/operando/garum/annotations/DefaultLong.java)
* [@DefaultFloat](https://github.com/operando/Garum/blob/master/garum/src/main/java/com/os/operando/garum/annotations/DefaultFloat.java)
* [@DefaultBoolean](https://github.com/operando/Garum/blob/master/garum/src/main/java/com/os/operando/garum/annotations/DefaultBoolean.java)
* [@DefaultString](https://github.com/operando/Garum/blob/master/garum/src/main/java/com/os/operando/garum/annotations/DefaultString.java)
* [@DefaultStringSet](https://github.com/operando/Garum/blob/master/garum/src/main/java/com/os/operando/garum/annotations/DefaultStringSet.java)

デフォルト値として、以下の型については、**リソース定義したものを使用することも可能**です。

* int
* booelan
* String
* Set<String>

```java
@Pref(name = "app_status")
public class AppStatus extends PrefModel {

    @PrefKey
    @DefaultString("test")
    public String appName;

    @PrefKey
    @DefaultInt(redId = R.Integer.test_integer)
    public int startupCount;

    @PrefKey
    @DefaultBoolean(false)
    public boolean showNotification;
}
```

# Garumの初期化

作成したModelを使用するには、ライブラリにModelを読み込ませる必要があります。

ApplicationクラスのonCreateメソッドなどに、**Garum.initialize**メソッドを書くことでModelの読み込みが自動的に行われます。

```java
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Garum.initialize(getApplicationContext());
    }
}
```

ActiveAndroidを使用したことがある方ならお分かりいただけると思いますが、**Modelの読み込みには時間がかかります**。

クラスの数が多いアプリに関しては、Modelの自動読み込みはものすごく時間がかかります。

ActiveAndroidと同様、この読み込みを指定したModelだけにする方法を用意しております。

例では、AppStatus/PrefModel/UseStatusクラスというModelだけを読み込み対象に絞っております。

こうすることで、**Modelの読み込み時間を短縮**することができます。

```java
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Context context = getApplicationContext();
        Configuration.Builder builder = new Configuration.Builder(context);
        builder.setModelClasses(AppStatus.class, PrefModel.class, UseStatus.class);
        Garum.initialize(builder.create());
    }
}
```

# saveメソッド - 値を保存する

ではでは、Modelの作成もして読み込みも終わりましたので、後は値の保存だけです。

先ほど作成したModelをインスタンス化し、saveメソッドを実行することで、フィールドで保持している値がSharedPreferencesに保存されます。

```java
AppStatus appStatus = new AppStatus();
appStatus.appName = "Garum";
appStatus.startupCount = ++appStatus.startupCount;
appStatus.showNotification = true;
appStatus.save();
```

saveメソッドを実行することで、以下の場所にXMLが作られ、フィールドの値が書き出されます。

**/data/data/[package name]/shared_prefs/app_status.xml**

```xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="appName">Garum</string>
    <int name="startupCount" value="1" />
    <boolean name="showNotification" value="true" />
</map>
```

保存した値を利用したい時には、Modelをインスタンス化してフィールドから値を取得するだけです。

インスタンス化した際に、**SharedPreferencesのXMLから値を取得して、フィールドへ自動的に代入**されます。

```java
AppStatus appStatus = new AppStatus();
if(appStatus.showNotification){
    // Notificationを出す処理...とかとか
}
```

# Type Serializerのサポート

通常のSharedPreferencesでは**サポートされていない型を保存できる**ような仕組みを提供しています。

| クラス | Deserialize | Serialize |
|:-----------:|:------------:|:------------:|
| DateSerializer | java.util.Date | Long |
| CalendarSerializer | Calendar | Long |
| FileSerializer | File | String |
| UriSerializer | Uri |  String  |

Modelの作成時に、Deserializeの型のフィールドを定義することで使用することができます。

```java
@Pref(name = "user_status")
public class UseStatus extends PrefModel {

    @PrefKey("last_used")
    public Date lastUsed;

    @PrefKey
    public Calendar birthday;

    @PrefKey("tmp_file")
    public File tmpFile;

    @PrefKey("uri")
    public Uri id_uri;
}
```

後は、それぞれフィールドへ代入してsaveメソッドで保存するだけです。

値を使用する時は、特に特別なことはなくインスタンス化するだけです。

ライブラリの内部でSerialize/Deserializeの処理を行っているので、難しいことは考えずに使えます。  

```java
UseStatus useStatus = new UseStatus();
useStatus.lastUsed = new Date();
useStatus.birthday = Calendar.getInstance();
useStatus.tmpFile = new File("tmp.txt");
useStatus.id_uri = Uri.parse("content://com.os.operando.sample/users/1");
useStatus.save();
```

また、各Serializerが実装しているTypeSerializerインターフェイスを使用することで、**自身の好きな型を保存することも可能**です。

最終的に**SharedPreferencesがサポートしている型にSerializeできれば、どんな型でも**保存することができます。

実装方法については、コードを見るのが一番早いような気がしますので、Githubに上がっているコードを参照してください。

**https://github.com/operando/Garum/tree/master/garum/src/main/java/com/os/operando/garum/serializers**


# Garumの課題点

こんな感じで今までのSharedPreferencesを意識せずにデータの管理が可能になります。

**Utilクラスも不要！Keyの定数も不要！**という最高な状態です！

しかし、GarumにはGarumで**数えきれないほどの課題**を抱えております。

直近でぶつかっている課題点では、以下のようなことです。

* 実行速度の問題。使いやすくなった分、SharedPreferencesをゴリゴリ使うよりはるかにGarumは遅い
* [PreferenceFragment](http://developer.android.com/reference/android/preference/PreferenceFragment.html)への対応
* [contains](http://developer.android.com/reference/android/content/SharedPreferences.html#contains(java.lang.String))メソッドがない。Keyの有無が判定しずらい。containsメソッドを実装すると結局Keyを意識するはめに
* [remove](http://developer.android.com/reference/android/content/SharedPreferences.Editor.html#remove(java.lang.String))メソッドもない
* @Default...アノテーション類の値指定で、valueとredIdどちらも定義されていたらどっちを優先すべきか。今はredIdを優先している

実装以外でも運用の問題を抱えています。これは私の経験不足なだけですが！


# まとめ

長々と書きましたが、まあ一言でまとめると

**「Garum使ってね！ダメダメなライブラリだから、みんなPRしてね★」**

ということです。

当分の間、SharedPreferencesで日々悩み続けるAndroidエンジニアでいられそうです。