1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Riverpodを使ってSharedPreferencesの値の管理をシンプルに実現したい

Last updated at Posted at 2025-02-09

Riverpodを使ってSharedPreferencesの値の管理をシンプルに実装する方法を考えました。

実装パターン1: setValues()、removeValues()関数で値を更新する

SharedPreferencesを初期化するProviderを定義

final rawSharedPreferenceProvider = FutureProvider(
  (_) async => SharedPreferences.getInstance(),
);

keyを定義

const sampleIntKey = 'sample_int';
const sampleBoolKey = 'sample_bool';
const sampleStringKey = 'sample_string';

Preferencesクラスを定義

setValues()、removeValues()関数を定義します。

class Preferences {
  Preferences({
    required this.ref,
    required this.sampleInt,
    required this.sampleBool,
    required this.sampleString,
  });

  final Ref ref;
  final int sampleInt;
  final bool sampleBool;
  final String sampleString;

  // 引数に値が渡された変数のみ更新する。nullのときは何もしない。
  Future<void> setValues({
    sampleInt? sampleInt,
    sampleBool? sampleBool,
    sampleString? sampleString,
  }) async {
    final sp = await SharedPreferences.getInstance();
    if (sampleInt != null) await sp.setInt(sampleIntKey, sampleInt);
    if (sampleBool != null) await sp.setBool(sampleBoolKey, sampleBool);
    if (sampleString != null) await sp.setString(sampleStringKey, sampleString);
    ref.invalidate(rawSharedPreferenceProvider);
  }

  // 引数にtrueが渡された項目を削除する
  Future<void> removeValues({
    bool? sampleInt,
    bool? sampleBool,
    bool? sampleString,
  }) async {
    final sp = await SharedPreferences.getInstance();
    if (sampleInt == true) await sp.remove(sampleIntKey);
    if (sampleBool == true) await sp.remove(sampleBoolKey);
    if (sampleString == true) await sp.remove(sampleStringKey);
    ref.invalidate(rawSharedPreferenceProvider);
  }
}

Preferencesを提供するProviderを定義

final prefsProvider = Provider((ref) {
  final sp = ref.watch(rawSharedPreferenceProvider).value;
  if (sp == null) return null;

  return Preferences(
    ref: ref,
    sampleInt: sp.getInt(sampleIntKey) ?? 1,
    sampleBool: sp.getBool(sampleBoolKey) ?? true,
    sampleString: sp.getString(sampleStringKey) ?? "default value",
  );
});

値の取得、更新

final prefs = ref.watch(prefsProvider);
// 取得
final sampleInt = prefs.sampleInt;
// 更新
await prefs.setValues(sampleInt: 2, sampleBool: false);
// 削除
await prefs.removeValues(sampleInt: true, sampleBool: true);

メリット

  • SharedPreferenceの値の操作が型安全になる
  • キーを意識しなくて済む(タイポも発生しない)
  • ジャンプしやすい

デメリット

  • 各値にgetterとsetterをもたせるよりは直感的でない

実装パターン2: AsyncNotifierで値を管理する

値の型と参照キーを定義するオブジェクトの作成

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');

Preferenceオブジェクトを定義する

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>(SpKey<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の値の操作が型安全になる
  • 項目を追加するときの手間が少ない

デメリット

  • 定義が面倒
  • 生成するのが分かりづらい。ジャンプもしづらい
  • 値の書き込みが直感的にできない(ずっと使っていても慣れない)

実装パターン3: getterとsetterで値を更新する

複数の値の更新時に問題が出るので没にしましたが、紹介だけさせていただきます。

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を配信しているのでそちらも見ていただけたら嬉しいです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?