はじめに
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メソッドを使った場合には、Modeを自身で指定しないといけません。 この時に、以下のModeを指定してしまうと、他のアプリから作成したファイルへの読み書きが可能になってしまいます。 ●MODE_WORLD_READABLE ●MODE_WORLD_WRITEABLE ちなみに上記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メソッドと比べると、引数が少ないことやファイル名を自身で考えなくてもいいという点があります。
package com.os.operando.sharedpreferences.sample;
SharedPreferences sp = getSharedPreferences("app_pref", MODE_PRIVATE);
// → /data/data/[package name]/shared_prefs/app_pref_preferences.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();
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());
}
}
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>
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;
}
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();