はじめに
こんにちは @operandoOS です。
この記事は Android Advent Calendar 2014 の11日目の記事です。
今回はAnnotation styleでSharedPreferencesが扱える**Garum**というライブラリを作ってるので、その話をしたいと思います。
無駄話が長すぎるので、Garumの説明から読みたい方は、以下をクリックしてください!
SharedPreferencesのおさらい
もうみなさんご存知かと思いますが、SharedPreferencesについて軽くおさらいします。
SharedPreferencesは、簡単にまとめると以下のような感じです。
- Androidにおける、アプリケーション内でデータ保存する方法の一つ
- アプリケーションのアンインストール、またはデータ削除を行うまで永続的にデータを保存できる
- key-value形式でデータを保存する
- 実データはXML形式で保存されている
使い方については、API Guides -> Storage Options -> Using Shared Preferences を参照してください。
SharedPreferencesのつらみ
SharedPreferencesはとても使いやすくて、わかりやすいAPIなのですが、ちょっとしたつらみがいくつかあります。
そのSharedPreferencesのつらみをいくつかあげたいと思います。
**「お前のつらみなんてどうでもいいよ!」**という方は、読み飛ばしてください。
**「端的に述べよ」**という方は、下の表を読んでそれぞれの詳細は読み飛ばしてください。
問題 | なぜ辛い? |
---|---|
Utilクラスパターン | Utilクラスの実装をもうしたくない。好みが出るので辛い。ほぼわがまま。 |
Mode問題 | セキュアじゃないModeを使う人がいて辛い。 |
getDefaultSharedPreferences問題 | 全ての値を一つのSharedPreferencesで管理する。数が増えると辛い。 |
Keyのベタ書き問題 | Keyの名前を定数化せず、色んなところでベタ書きしてる。変更に弱いので辛い。 |
※あくまで個人的なつらみ
Utilクラスパターン
SharedPreferencesのUtilクラスでよく見るパターンが2つあります。
- Keyごとのメソッドがあるパターン - アプリ特化型
- Keyを引数で渡すパターン - 汎用型
ちょっと長いですが、この2つのパターンについて説明したいと思います。
Keyごとのメソッドがあるパターン - アプリ特化型
Keyごとにメソッド(アクセッサー)を作成し、SharedPreferencesの処理は全てUtilクラス内部で操作する実装です。
Keyを意識せず、Utilクラスを使う人にSharedPreferencesをあまり意識させないように設計しています。
例として、Google I/Oのコードである**ioschedのPrefUtils**を見ると、このパターンの特徴がわかります。
このパターンを**「アプリ特化型」**と勝手に読んでます。
Keyの名前はアプリごとに変わります。Keyの名前も数もアプリごとに違うはずです。
このパターンのUtilクラスは、特定アプリのKeyに依存した実装になっているため、アプリ特化型と読んでいます。
Keyを引数で渡すパターン - 汎用型
Keyを引数で渡し、SharedPreferencesの処理は全てUtilクラス内部で操作する実装です。
こちらは先ほどのパターンと違い、使う人にKeyは意識させますがSharedPreferencesはあまり意識させないように設計してます。
例として、みんな大好きRebuildのコードであるRebuildのPreferenceUtilsを見ると、このパターンの特徴がわかります。
ioschedのPrefUtilsとの違いは、Keyごとのメソッドは存在しません。型ごとのメソッドが存在し、そこにKeyを引数で渡す実装になってます。
このパターンを**「汎用型」**と勝手に読んでます。
Keyの名前はアプリごとに変わりますが、SharedPreferencesの処理は変わりません。
変わらない部分の操作をUtil化したパターンというわけです。こちらの方がUtilというには相応しいかもしれません。
このパターンのUtilクラスを実装すると、どのアプリでも使用できるUtilクラスが作成できます。
Mode問題 - ファイル等を作成する際に指定するパーミッション
getDefaultSharedPreferencesメソッドを使用した場合には、常にModeがMODE_PRIVATEで作成されます。
MODE_PRIVATEで作成したファイルであれば、自身のアプリからしか読み書きできないようになるので問題ありません。
しかし、[getSharedPreferences](http://developer.android.com/reference/android/content/Context.html#getSharedPreferences(java.lang.String, int))メソッドを使った場合には、Modeを自身で指定しないといけません。
この時に、以下のModeを指定してしまうと、他のアプリから作成したファイルへの読み書きが可能になってしまいます。
ちなみに上記2つのModeは、API Level 17から非推奨になっています。
このMode対策の一つとして、Modeやパーミッションに詳しくない人でも問題なく使えるようにするため、先ほどのUtilクラス内でModeをMODE_PRIVATEに固定するという手法があります。
セキュリティを意識しなくていい!という話ではありませんが、このようにすると対策にはなると思います。
getDefaultSharedPreferences問題
getDefaultSharedPreferencesメソッドは非常に使いやすいです。
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))メソッドと比べると、引数が少ないことやファイル名を自身で考えなくてもいいという点があります。
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の定義を定数で宣言しないと、以下の例のように変更に弱いコードになってしまいます。
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**です。
Garum - https://github.com/operando/Garum
Garumは、アノテーションを使用してSharedPreferencesのkey-value形式の読み書きを簡単にするものです。
値の書き込みは、アクセッサーやフィールドへの代入で行います。
値の読み込みは、後ほど紹介するModelをインスタンス化した際に自動的に読み込まれる仕組みになっています。
Garumの実装は、使ってみればわかりますが実は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アノテーションについては、以下を参照ください。この記事では割愛させていただきます。
例では、フィールドのクセス修飾子がpublicですが、privateなどでも大丈夫です。
privateにした場合は、Lombokの@Dataアノテーションを指定して、アクセッサーを自動生成することをオススメします。
@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が保存されていなかった際のデフォルト値をフィールドに指定できるアノテーションも用意しています。
デフォルト値として、以下の型については、リソース定義したものを使用することも可能です。
- int
- booelan
- String
- Set
@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の読み込みが自動的に行われます。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Garum.initialize(getApplicationContext());
}
}
ActiveAndroidを使用したことがある方ならお分かりいただけると思いますが、Modelの読み込みには時間がかかります。
クラスの数が多いアプリに関しては、Modelの自動読み込みはものすごく時間がかかります。
ActiveAndroidと同様、この読み込みを指定したModelだけにする方法を用意しております。
例では、AppStatus/PrefModel/UseStatusクラスというModelだけを読み込み対象に絞っております。
こうすることで、Modelの読み込み時間を短縮することができます。
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に保存されます。
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 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から値を取得して、フィールドへ自動的に代入されます。
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の型のフィールドを定義することで使用することができます。
@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の処理を行っているので、難しいことは考えずに使えます。
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への対応
- containsメソッドがない。Keyの有無が判定しずらい。containsメソッドを実装すると結局Keyを意識するはめに
- removeメソッドもない
- @Default...アノテーション類の値指定で、valueとredIdどちらも定義されていたらどっちを優先すべきか。今はredIdを優先している
実装以外でも運用の問題を抱えています。これは私の経験不足なだけですが!
まとめ
長々と書きましたが、まあ一言でまとめると
「Garum使ってね!ダメダメなライブラリだから、みんなPRしてね★」
ということです。
当分の間、SharedPreferencesで日々悩み続けるAndroidエンジニアでいられそうです。