【Flutter】SharedPreferences におけるキー管理のベストプラクティス

はじめに
モバイルアプリケーション開発において、アプリケーションの設定やユーザーの状態をローカルに保存するために、Flutter では shared_preferences プラグインを利用することが多いと思います。その際、みなさんはキー管理をどのように行っていますでしょうか。この記事では、shared_preferences を利用する際のキー管理について筆者が考える方法を紹介します。
shared_preferences プラグインとは
shared_preferences プラグインとは、プラットフォーム固有の永続ストレージをラップした Flutter プラグインです。shared_preferences という名前ですが実際の保存先はプラットフォームによって異なります。Android では SharedPreferences、iOS では UserDefaults が利用されます。以降、Flutter の shared_preferences プラグインとして、shared_preferences と表記します。
shared_preferences の利用例
サポートされているデータ型は int
、bool
、double
、String
、List<String>
です。Write と Read、Remove の操作が可能で、データの Write および Read
操作時には、「引数でキー」を「メソッドで対応するデータ型」を指定する必要があります。以下のコードは、公式のexampleです。
// Obtain shared preferences.
final SharedPreferences prefs = await SharedPreferences.getInstance();
// Save an integer value to 'counter' key.
await prefs.setInt('counter', 10);
// Save an boolean value to 'repeat' key.
await prefs.setBool('repeat', true);
// Save an double value to 'decimal' key.
await prefs.setDouble('decimal', 1.5);
// Save an String value to 'action' key.
await prefs.setString('action', 'Start');
// Save an list of strings to 'items' key.
await prefs.setStringList('items', <String>['Earth', 'Moon', 'Sun']);
// Try reading data from the 'counter' key. If it doesn't exist, returns null.
final int? counter = prefs.getInt('counter');
// Try reading data from the 'repeat' key. If it doesn't exist, returns null.
final bool? repeat = prefs.getBool('repeat');
// Try reading data from the 'decimal' key. If it doesn't exist, returns null.
final double? decimal = prefs.getDouble('decimal');
// Try reading data from the 'action' key. If it doesn't exist, returns null.
final String? action = prefs.getString('action');
// Try reading data from the 'items' key. If it doesn't exist, returns null.
final List<String>? items = prefs.getStringList('items');
// Remove data for the 'counter' key.
await prefs.remove('counter');
キー管理について
前章の利用例のとおり、shared_preferences ではデータ取得、保存、削除にはキーが必要で、取得、保存時にはそのキーに対応するデータ型を指定する必要があります。また、キーをタイポしたり、異なるデータ型を指定すると、予期せぬバグが発生する可能性があります。そのため、キーと対応する型は、アプリケーション全体で一貫性を持たせることが重要です。
キー管理の方法とその実装
キーの管理方法は様々あると思いますが、筆者が考える方法を以下に示します。重要なのは、キーと対応するデータ型を一元管理することです。
enum SharedPreferencesKey<T> {
counter<int>('counter'),
repeat<bool>('repeat'),
decimal<double>('decimal'),
action<String>('action'),
items<List<String>>('items'),
const SharedPreferencesKey(this.str);
final String str;
}
上記のように、SharedPreferencesKey
というEnumを定義し、キーと対応するデータ型を一元管理します。このようにすることで、キーと対応するデータ型を一元管理することができます。また、キーのタイポを防ぐことができます。
SharedPreferences のラッパークラスの作成
先ほど定義した SharedPreferencesKey
を利用して、SharedPreferences
のラッパークラスを作成します。以下のコードは、SharedPreferences
のラッパークラスの例です。
class MySharedPreferences {
MySharedPreferences(this._prefs);
final SharedPreferences _prefs;
@override
Future<T?> get<T>(SharedPreferencesKey<T> key) async {
late final dynamic value;
if (T == String) {
value = _prefs.getString(key.str);
} else if (T == int) {
value = _prefs.getInt(key.str);
} else if (T == double) {
value = _prefs.getDouble(key.str);
} else if (T == bool) {
value = _prefs.getBool(key.str);
} else if (T == List<String>) {
value = _prefs.getStringList(key.str);
} else {
throw Exception('Unsupported type');
}
if (value is T?) {
return value;
}
return null;
}
@override
Future<void> set<T>(SharedPreferencesKey<T> key, T value) async {
if (value is String) {
await _prefs.setString(key.str, value);
} else if (value is int) {
await _prefs.setInt(key.str, value);
} else if (value is double) {
await _prefs.setDouble(key.str, value);
} else if (value is bool) {
await _prefs.setBool(key.str, value);
} else if (value is List<String>) {
await _prefs.setStringList(key.str, value);
} else {
throw Exception('Unsupported type');
}
}
@override
Future<void> remove<T>(SharedPreferencesKey<T> key) async {
await _prefs.remove(key.str);
}
@override
Future<void> clear() async {
await _prefs.clear();
}
}
この実装では、generics
を利用することで、データ型ごとにメソッドを定義する必要がなくなり、コードの冗長性を減らすことができます。また、型パラメータ
T
は SharedPreferencesKey
で定義した型や引数として受け取った型から、推論されるため、MySharedPreferences
の利用時には保存、取得するデータ型を指定する必要がありません。
MySharedPreferences の利用例
では、# shared_preferences の利用例
で紹介したコードを MySharedPreferences
を利用して書き換えてみましょう。
final prefs = await SharedPreferences.getInstance();
final myPrefs = MySharedPreferences(prefs);
// Save an integer value to 'counter' key.
await myPrefs.set(SharedPreferencesKey.counter, 10);
// Save an boolean value to 'repeat' key.
await myPrefs.set(SharedPreferencesKey.repeat, true);
// Save an double value to 'decimal' key.
await myPrefs.set(SharedPreferencesKey.decimal, 1.5);
// Save an String value to 'action' key.
await myPrefs.set(SharedPreferencesKey.action, 'Start');
// Save an list of strings to 'items' key.
await myPrefs.set(SharedPreferencesKey.items, <String>['Earth', 'Moon', 'Sun']);
// Try reading data from the 'counter' key. If it doesn't exist, returns null.
final int? counter = await myPrefs.get(SharedPreferencesKey.counter);
// Try reading data from the 'repeat' key. If it doesn't exist, returns null.
final bool? repeat = await myPrefs.get(SharedPreferencesKey.repeat);
// Try reading data from the 'decimal' key. If it doesn't exist, returns null.
final double? decimal = await myPrefs.get(SharedPreferencesKey.decimal);
// Try reading data from the 'action' key. If it doesn't exist, returns null.
final String? action = await myPrefs.get(SharedPreferencesKey.action);
// Try reading data from the 'items' key. If it doesn't exist, returns null.
final List<String>? items = await myPrefs.get(SharedPreferencesKey.items);
// Remove data for the 'counter' key.
await myPrefs.remove(SharedPreferencesKey.counter);
キーやデータ型を意識せずに、SharedPreferencesKey
を利用して、データの保存、取得、削除を行うことができます。また、キーとデータ型の一元管理により、キーのタイポを防ぐことができます。
まとめ
この記事では、shared_preferences
を利用する際のキー管理について紹介しました。キーと対応するデータ型を一元管理することで、キーのタイポを防ぎ、アプリケーション全体で一貫性を持たせることができます。また、MySharedPreferences
のようなラッパークラスを作成することで、コードの冗長性を減らすことができます。キー管理について悩んでいる方は、ぜひ参考にしてみてください。