【flutter】SharedPreferenceRepositoryを作った / getInstance()が非同期である問題の対処
概要
SharedPreferenceRepository
なるものを作って、永続化ロジックをカプセル化しようとしたところ、flutterのconstructorはawaitできないという部分で困りました。
class SharedPreferenceRepository {
static SharedPreferences _prefs; // ← どうやってinitする?
String get username => _prefs.getString("username");
....
}
というのも、shared_preferences
パッケージの getInstance()
は非同期関数なのです。
// 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
Singleton
今回は後者のSingletonパターンのうち、factoryを用いた方法で実装してみました。(Provider
はconsumeできる場所が限られるので)
実装
まずは SharedPreference
のインスタンスをキャッシュします。最初の取得は init()
関数に切り出して main
の runApp()
の前に await
します。
_prefs
はnullableになっていますが、 これで runApp()
後に null
にはならないはずです。
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());
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
をより綺麗に書き換えていきます。
設計に関しては以下の記事を大いに参考にしました。key
を String
ではなく enum
にする点と、各ドメインオブジェクトに対してgetterとsetterを設ける点です。
また、sharedPreference向けにデータを整形するserializerとdeserializerをまとめて Converter
型としています。このアイデアはfirestoreの withConverter
を参考にしました。
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です。
import 'package:meta/meta.dart';
class Hoge {
const Hoge({required this.value});
final String value;
}
Kotlinの ?.let
を使うために以下のextensionもお借りしました。
extension LetExt<T> on T {
R let<R>(R Function(T) block) => block(this);
}
Object
複雑なドメインオブジェクトをそのまま書き込みたい時は、 ドメイン → JSON → String と変換して書き込みます。
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は fromJson
や toJson()
をいい感じに生成してくれるので大変便利。
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