hydrated_mobx
MobX ステート管理ライブラリの拡張で、MobX ステートを自動的に永続化および復元します。
Flutterパッケージで、MobXストアを自動的に永続化および復元します。Flutterの状態管理ソリューションであるMobXと連携するように設計されています。
^2.0.0^1.3.0^2.5.0^3.0.0^2.3.3^1.17.1^3.0.2^2.3.2^2.2.0^4.8.0^6.6.1^1.0.0^1.8.3^1.22.2^3.0.7以下は英語原文のスナップショットです。最新版は GitHub をご覧ください。
A Flutter package that automatically persists and restores MobX stores. Built to work with Flutter's state management solution MobX.
This package uses some code from hydrated_bloc by Felix Angelov, which is licensed under the MIT License. We extend our gratitude to the original authors for their work.
migrate hook for evolving persisted stateimportDataAdd the package to your pubspec.yaml:
dependencies:
hydrated_mobx: ^1.2.0
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_mobx/hydrated_mobx.dart';
import 'package:path_provider/path_provider.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDir = await getApplicationDocumentsDirectory();
HydratedMobX.storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory(appDocumentDir.path),
);
runApp(App());
}
import 'package:mobx/mobx.dart';
import 'package:hydrated_mobx/hydrated_mobx.dart';
part 'counter_store.g.dart';
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore extends HydratedMobX with Store {
@observable
int count = 0;
@action
void increment() => count++;
@override
Map<String, dynamic>? toJson() => {'count': count};
@override
void fromJson(Map<String, dynamic> json) {
count = json['count'] as int;
}
}
When you have multiple instances of the same store (e.g. one per meeting or per user), pass the id to super(storeId: ...) so hydration uses the correct key. This works with dependency injection (e.g. injectable):
abstract class _MeetingStoreBase extends HydratedMobX with Store {
_MeetingStoreBase(
DioService dioService,
MeetingsStore meetingsStore,
@factoryParam String meetingId,
) : _meetingId = meetingId,
super(storeId: meetingId);
final String _meetingId;
@override
String get id => _meetingId;
// ...
}
See the example app’s KeyedCounterStore for a minimal example.
To avoid manual type checks and try/catch in fromJson/toJson, use the HydratedJson helpers:
import 'package:hydrated_mobx/hydrated_mobx.dart';
import 'package:mobx/mobx.dart';
@override
void fromJson(Map<String, dynamic> json) {
_meetings = HydratedJson.readList(
json,
'meetings',
MeetingDto.fromJson,
).asObservable();
_meta = HydratedJson.readObject(json, 'meta', MetaDto.fromJson) ??
MetaDto(take: 20);
}
@override
Map<String, dynamic> toJson() => {
'meetings': HydratedJson.writeList(_meetings, (e) => e.toJson()),
'meta': _meta.toJson(),
};
Available helpers: readList, readObject, readString, readInt, readDouble, readBool, writeList. They return safe defaults (e.g. empty list, 0, null) when the key is missing or the value has the wrong type.
When the shape of your persisted state changes between app releases, bump
version and implement migrate to upgrade older data. migrate is called
during hydration whenever the stored version is lower than the current one; its
result is passed to fromJson and re-persisted under the new version, so it runs
only once per upgrade. Data written before versioning existed is treated as
version 1.
class CounterStore extends HydratedMobX with Store {
CounterStore() { hydrate(); }
final Observable<int> _count = Observable(0);
int get count => _count.value;
@override
int get version => 2;
@override
Map<String, dynamic> migrate(int oldVersion, Map<String, dynamic> old) {
if (oldVersion < 2) {
// v1 stored the value under 'counter'; v2 renamed it to 'count'.
old['count'] = old.remove('counter') ?? 0;
}
return old;
}
@override
Map<String, dynamic>? toJson() => {'count': _count.value};
@override
void fromJson(Map<String, dynamic> json) =>
_count.value = (json['count'] as int?) ?? 0;
}
When jumping multiple versions at once, apply every intermediate step in order
with stacked if (oldVersion < N) blocks. Dart switch cases do not fall
through, so a switch would skip the intermediate upgrades.
To bring data in from another persistence layer — SharedPreferences, a legacy
Hive box, or a previous key scheme — seed the storage with
HydratedMobX.importData after storage is set and before you construct the
stores that should pick it up. Keys map to each store's storageToken,
composed as '$storagePrefix${storeId ?? id}' (include the storeId/id
component when the target store overrides it, and note that the default
storagePrefix is runtimeType, which is not stable under --obfuscate —
override storagePrefix on such stores so the key matches). Existing keys are
left untouched by default; pass overwrite: true to replace them.
If the store uses schema versioning (version > 1), pass the version of the
data you are importing so it is not needlessly run through migrate; leave it
at the default (1) for legacy data that should be migrated forward.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedMobX.storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory(
(await getApplicationDocumentsDirectory()).path,
),
);
final prefs = await SharedPreferences.getInstance();
await HydratedMobX.importData({
'CounterStore': {'count': prefs.getInt('count') ?? 0},
});
final store = CounterStore(); // hydrates from the imported data
runApp(App());
}
State is persisted in the background (fire-and-forget), so a failing write never
interrupts your store. To observe such failures — a full disk, an encryption
error — pass an onStorageError handler. It defaults to logging the error.
class CounterStore extends HydratedMobX with Store {
CounterStore()
: super(
onStorageError: (error, stackTrace) {
FirebaseCrashlytics.instance.recordError(error, stackTrace);
},
) {
hydrate();
}
// ...
}
Note on multiple instances: the default storage key is derived from the store's
runtimeType, so two live instances of the same store type share one key and overwrite each other. If you intentionally keep several instances of the same type, overrideid(or passstoreId) to give each a distinct key.
clear() deletes the store's cached state and, by default, stops
persisting further changes.clear(resume: true) deletes the cached state but keeps persisting, so
subsequent changes are saved again.dispose() permanently stops persistence and removes the store from the
internal registry so it can be garbage collected. Call it when the store is
no longer used (e.g. from the owning widget's dispose); the on-disk state is
left untouched.await store.clear(); // wipe cache, stop persisting
await store.clear(resume: true); // wipe cache, keep persisting
store.dispose(); // stop persisting for good, keep stored data
Call dispose() when a store is scoped to a widget. A persisting store is
kept alive by its internal MobX reaction (this is how it observes changes), so a
store you simply drop is not garbage-collected until that reaction is released.
Calling dispose() releases it immediately. For a store owned by a State,
dispose it from the widget's dispose:
class _MyPageState extends State<MyPage> {
final store = CounterStore();
@override
void dispose() {
store.dispose();
super.dispose();
}
// ...
}
Long-lived, app-wide singleton stores don't need this — they live for the whole session by design.