🥶

【flutter】SharedPreferenceRepositoryを作った / getInstance()が非同期である問題の対処

2023/07/09に公開

概要

SharedPreferenceRepository なるものを作って、永続化ロジックをカプセル化しようとしたところ、flutterのconstructorはawaitできないという部分で困りました。

shared_preference_repository.dart
class SharedPreferenceRepository {
  static SharedPreferences _prefs; // ← どうやってinitする?
  
  String get username => _prefs.getString("username");
  ....
}

というのも、shared_preferences パッケージの getInstance() は非同期関数なのです。
https://pub.dev/packages/shared_preferences

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

少しリサーチすると、先人たちが解決策を挙げてくれています。

Provider

https://zenn.dev/riscait/books/flutter-riverpod-practical-introduction-archive/viewer/v0-shared-preferences

Singleton

https://simondev.medium.com/use-sharedpreferences-in-flutter-effortlessly-835bba8f7418

今回は後者のSingletonパターンのうち、factoryを用いた方法で実装してみました。(Provider はconsumeできる場所が限られるので)

実装

まずは SharedPreference のインスタンスをキャッシュします。最初の取得は init() 関数に切り出して mainrunApp() の前に await します。

_prefs はnullableになっていますが、 これで runApp() 後に null にはならないはずです。

shared_preference_repository.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferenceRepository {
  static SharedPreferences? _prefs;

  factory SharedPreferenceRepository() =>
      SharedPreferenceRepository._internal();

  SharedPreferenceRepository._internal();

  Future<void> init() async {
    _prefs ??= await SharedPreferences.getInstance();
  }
  
  ...
}

// RiverPodで使う用
final sharedPreferenceRepositoryProvider =
    Provider((ref) => SharedPreferenceRepository());
main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await SharedPreferenceRepository().init();

  // // Shared Preferenceが無茶苦茶になったらこれでリセット
  // final preferences = await SharedPreferences.getInstance();
  // await preferences.clear();

  runApp(const ProviderScope(child: MainApp()));
}

SharedPreferenceRepository

ここからは SharedPreferenceRepository をより綺麗に書き換えていきます。

設計に関しては以下の記事を大いに参考にしました。
https://blog.dalt.me/2356
参考にしたのは、 keyString ではなく enum にする点と、各ドメインオブジェクトに対してgetterとsetterを設ける点です。

また、sharedPreference向けにデータを整形するserializerとdeserializerをまとめて Converter 型としています。このアイデアはfirestoreの withConverter を参考にしました。
https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FirestoreDataConverter

shared_preference_repository.dart
class SharedPreferenceRepository {
  static SharedPreferences? _prefs;

  factory SharedPreferenceRepository() =>
      SharedPreferenceRepository._internal();

  SharedPreferenceRepository._internal();

  Future<void> init() async {
    _prefs ??= await SharedPreferences.getInstance();
  }

  /// T: シリアライズ前の型, U: シリアライズ後の型
  static PrefRepoItem<T> _createItem<T, U>(
          _PrefsKey key,
          _Converter<T, U> converter,
          Future<bool> Function(String, U)? setter,
          U? Function(String)? getter) =>
      PrefRepoItem<T>(
        set: (val) =>
            setter?.let((func) => func(key.name, converter.serialize(val))) ??
            Future(() => false),
        get: () => getter?.let(
            (func) => func(key.name)?.let((val) => converter.deserialize(val))),
      );

  // Stringとして保存
  static PrefRepoItem<T> _strItem<T>(
          _PrefsKey key, _Converter<T, String> converter) =>
      _createItem<T, String>(
          key, converter, _prefs?.setString, _prefs?.getString);
	 
  // intとして保存
  static PrefRepoItem<T> _intItem<T>(
          _PrefsKey key, _Converter<T, int> converter) =>
      _createItem<T, int>(key, converter, _prefs?.setInt, _prefs?.getInt);
  
  ...

  // それぞれの値はこんな感じで作成してゆく
  final hoge = _strItem<Hoge>(
      _PrefsKey.hoge,
      _Converter(
          serialize: (val) => val.value,
          deserialize: (val) => Hoge(value: val)));
  ...
}

/// key
enum _PrefsKey {
  hoge,
  fuga,
  piyo,
  ...
}

/// setterとgetterの組
class PrefRepoItem<T> {
  Future<bool> Function(T value) set;
  T? Function() get;

  PrefRepoItem({
    required this.set,
    required this.get,
  });
}

/// serializerとdeserializerの組
class _Converter<T, U> {
  U Function(T) serialize;
  T Function(U) deserialize;

  _Converter({
    required this.serialize,
    required this.deserialize,
  });
}

ここで Hoge 型は String のwrapperです。

hoge.dart
import 'package:meta/meta.dart';


class Hoge {
  const Hoge({required this.value});

  final String value;
}

Kotlinの ?.let を使うために以下のextensionもお借りしました。
https://zenn.dev/link/comments/b1c04aca7b9c48

utils.dart
extension LetExt<T> on T {
  R let<R>(R Function(T) block) => block(this);
}

Object

複雑なドメインオブジェクトをそのまま書き込みたい時は、 ドメイン → JSON → String と変換して書き込みます。

shared_preference_repository.dart
import 'dart:convert';

class SharedPreferenceRepository {
  ...
  
  final hogeSetting = _strItem<HogeSetting>(
      _PrefsKey.hogeSetting,
      Converter(
          serialize: (val) => jsonEncode(val.toJson()),
          deserialize: (val) => HogeSetting.fromJson(jsonDecode(val))));
}

ここで、Setting型はfreezedで作成したvalueObjectを用いました。freezedは fromJsontoJson() をいい感じに生成してくれるので大変便利。

hoge_setting.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'hoge_setting.freezed.dart';
part 'hoge_setting.g.dart';


class HogeSetting with _$HogeSetting {
  const HogeSetting._(); // メソッドを持たせたいなら必要

  factory HogeSetting(
      {required HogeSettingWidth width,
      required HogeSettingHeight height}) = _HogeSetting;

  factory HogeSetting.fromJson(Map<String, dynamic> json) =>
      _$HogeSettingFromJson(json);
  
  ...
}

まとめ

  • SharedPreference.getInstance() はアプリ起動時に await すると良い
  • SharedPreferenceのkeyはenumにすると良い
  • serialize/deserializeは Converter<T, U> として共通化すると見通しが良くなるかもしれない
  • 複雑なObjectは一旦JSONにしてStringifyすると良い
    • freezedが便利

Discussion