Riverpodを使ってSharedPreferenceの値の管理をシンプルに実装する方法を考えました。
実装パターン1(正攻法)
値の型と参照キーを定義するオブジェクトの作成
class SpKey<T> {
const SpKey(this.key);
final String key;
}
値の型とキーを定義する
const sampleInt = SpKey<int>('sample_int');
const sampleBool = SpKey<bool>('sample_bool');
const sampleString = SpKey<String>('sample_string');
AsyncNotifierで使うオブジェクトを定義する
part 'shared_preference.freezed.dart';
@freezed
class Preference with _$Preference {
factory Preference({
required int sampleInt,
required bool sampleBool,
required String sampleString
}) = _Preference;
}
AsyncNotifierで使うオブジェクトを定義する
part 'shared_preference.g.dart';
@riverpod
class SharedPreference extends _$SharedPreference {
@override
FutureOr<Preference> build() async {
return await generateLatestPreference();
}
Future<Preference> generateLatestPreference() async {
final sp = await SharedPreferences.getInstance();
return Preference(
sampleInt: sp.getInt('sample_int') ?? 2,
sampleBool: sp.getBool('sample_bool') ?? false,
sampleString: sp.getString('sample_string') ?? 'default',
);
}
Future<void> setValue<T>(SpKey2<T> spKey, T value) async {
final sp = await SharedPreferences.getInstance();
if (T == int) {
await sp.setInt(spKey.key, value as int);
} else if (T == bool) {
await sp.setBool(spKey.key, value as bool);
} else if (T == String) {
await sp.setString(spKey.key, value as String);
} else {
throw Exception('想定されていない型です。 spKey: ${spKey.key}');
}
state = AsyncValue.data(await generateLatestPreference());
}
}
メリット
- 値の複数更新をしてもエラーにならない
- SharedPreferenceの値の操作が型安全になる
- 項目を追加するときの手間が少ない
デメリット
- 定義が面倒
- 生成するのが分かりづらい。ジャンプもしづらい
実装パターン2
SharedPreferencesを初期化するProviderの作成
final rawSharedPreferenceProvider = FutureProvider(
(_) async => SharedPreferences.getInstance(),
);
各設定を扱う型を定義
class Preference<T> {
Preference({
required this.ref,
required this.key,
required this.defaultValue,
}) {
value = _getValue();
}
final Ref ref;
// SharedPreferenceで読み書きする際に使うキー
final String key;
// 設定がない場合の初期値
final T defaultValue;
// 設定値。コンストラクターで値を入れる
late final T value;
T _getValue() {
final prefs = ref.watch(rawSharedPreferenceProvider).value;
if (T == int) {
return prefs?.getInt(key) as T? ?? defaultValue;
} else if (T == bool) {
return prefs?.getBool(key) as T? ?? defaultValue;
} else if (T == String) {
return prefs?.getString(key) as T? ?? defaultValue;
} else {
throw Exception('想定されていない型です。');
}
}
Future<void> setValue(T newValue) async {
final prefs = ref.watch(rawSharedPreferenceProvider).value;
// 本当はnullチェックしたほうが良いと思います
if (newValue is int) {
await prefs?.setInt(key, newValue);
} else if (newValue is bool) {
await prefs?.setBool(key, newValue);
} else if (newValue is String) {
await prefs?.setString(key, newValue);
} else {
throw Exception('想定されていない型です。');
}
ref.invalidate(rawSharedPreferenceProvider);
}
}
getValue, setValueを定義して、値の型に関係なく読み書きできるようにします。値の更新後にref.invalidate(rawSharedPreferenceProvider)を実行しプロバイダを読み込みなおします。
全設定値を扱う型を定義
class Preferences {
Preferences({
required this.sampleInt,
required this.sampleBool,
required this.sampleString,
});
final Preference<int> sampleInt;
final Preference<bool> sampleBool;
final Preference<String> sampleString;
}
Preferenceで各項目を型付けできるようにします。
SharedPreferenceを管理するproviderの作成
final sharedPreferenceProvider = Provider<Preferences>((ref) {
return Preferences(
sampleInt: Preference(
ref: ref,
key: 'sample_int',
defaultValue: 0,
),
sampleBool: Preference(
ref: ref,
key: 'sample_bool',
defaultValue: false,
),
sampleString: Preference(
ref: ref,
key: 'sample_string',
defaultValue: 'sample',
),
);
});
この際にSharedPreferenceで読み書きする際に使うキーと値が存在しない場合の初期値を定義します。
値の取得、更新
final prefs = ref.watch(sharedPreferenceProvider);
// 値の取得
final sampleInt = prefs.sampleInt.value;
// 値の更新
await prefs.sampleInt.setValue(5);
メリット
- 値の読み書きが直感的にできる
- キーを意識しなくて済む(タイポも発生しない)
- SharedPreferenceの値の操作が型安全になる
- 項目を追加するときの手間が少ない
デメリット
- 複数の値を更新しようとするとエラーになる。(ref.invalidate()の実行中にref.invalidate()を実行できないため。)
おわりに
ここまで読んでいただけた方はいいねとストックよろしくお願いします。
Flutter製の便利すぎると好評の単語帳アプリBestflipを配信しているのでそちらも見ていただけたら嬉しいです。