Skip to main content

Persistence

When you instantiate your store, you can optionally pass it a persistor, which is an object that may be used for local persistence, i.e., keeping the current app state saved to the local disk of the device.

You should create your own MyPersistor class which extends the Persistor abstract class. This is the recommended way to use it:

var persistor = MyPersistor();          

var initialState = await persistor.readState();

if (initialState == null) {
initialState = AppState.initialState();
await persistor.saveInitialState(initialState);
}

var store = Store<AppState>(
initialState: initialState,
persistor: persistor,
);

As you can see above, when the app starts you use the persistor's readState method to try and read the state from the disk. If this method returns null, you must create the default initial state and save it. You then create the store with that initialState and the persistor.

This is the abstract Persistor definition:

abstract class Persistor<St> {
Future<St?> readState();
Future<void> deleteState();
Future<void> persistDifference({required St? lastPersistedState, required St newState});
Future<void> saveInitialState(St state) => persistDifference(lastPersistedState: null, newState: state);
Duration get throttle => const Duration(seconds: 2);
}

The persistDifference method is the one you should implement to be notified whenever you must save the state. It tells you the newState and the lastPersistedState, so that you can compare them and save the difference. Or, if your app state is simple, you can simply save the whole newState each time the method is called.

The persistDifference method will be called by Async Redux whenever the state changes, but not more than once each 2 seconds, which is the throttle period. All state changes within these 2 seconds will be collected, and then a single call to the method will be made with all the changes after this period.

Also, the persistDifference method won't be called while the previous save is not finished, even if the throttle period is done. In this case, if a new state becomes available the method will be called as soon as the current save finishes.

Note you can also override the throttle getter to define a different throttle period. In special, if you define it as null there will be no throttle, and you'll be able to save the state as soon as it changes.

Even if you have a non-zero throttle period, sometimes you may want to save the state immediately. This is usually the case, for example, when the app is closing. You can do that by dispatching the provided PersistAction. This action will ignore the throttle period and call the persistDifference method right away to save the current state.

store.dispatch(PersistAction());

You can use this code to help you start extending the abstract Persistor class:

class MyPersistor extends Persistor<AppState> {  

Future<AppState?> readState() async {
// TODO: Put here the code to read the state from disk.
return null;
}

Future<void> deleteState() async {
// TODO: Put here the code to delete the state from disk.
}

Future<void> persistDifference({
required AppState? lastPersistedState,
required AppState newState,
}) async {
// TODO: Put here the code to save the state to disk.
}

Future<void> saveInitialState(AppState state) =>
persistDifference(lastPersistedState: null, newState: state);

Duration get throttle => const Duration(seconds: 2);
}

The persistor can also be paused and resumed, with methods store.pausePersistor(), store.persistAndPausePersistor() and store.resumePersistor(). This may be used together with the app lifecycle, to prevent a persistence process to start when the app is being shut down.

When logging out of the app, you can call store.deletePersistedState() to ask the persistor to delete the state from disk.

First, add an AppLifecycleManager to your widget tree:

...
child: StoreProvider<AppState>(
store: store,
child: AppLifecycleManager(
child: MaterialApp( ...

Then, create the AppLifecycleManager with a WidgetsBindingObserver that dispatches a ProcessLifecycleChange_Action:

class AppLifecycleManager extends StatefulWidget {
final Widget child;
const AppLifecycleManager({Key? key, required this.child}) : super(key: key);
_AppLifecycleManagerState createState() => _AppLifecycleManagerState();
}

class _AppLifecycleManagerState extends State<AppLifecycleManager> with WidgetsBindingObserver {

void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

void didChangeAppLifecycleState(AppLifecycleState lifecycle) {
store.dispatch(ProcessLifecycleChange_Action(lifecycle));
}

Widget build(BuildContext context) => widget.child;
}

Finally, define your ProcessLifecycleChange_Action to pause and resume the persistor:

class ProcessLifecycleChangeAction extends ReduxAction<AppState> {
final AppLifecycleState lifecycle;
ProcessLifecycleChangeAction(this.lifecycle);

Future<AppState?> reduce() async {
if (lifecycle == AppLifecycleState.resumed || lifecycle == AppLifecycleState.inactive) {
store.resumePersistor();
} else if (lifecycle == AppLifecycleState.paused || lifecycle == AppLifecycleState.detached) {
store.persistAndPausePersistor();
} else
throw AssertionError(lifecycle);

return null;
}
}

Have a look at the: Persistence tests.


Saving and Loading

You can choose any way you want to save the state difference to the local disk, but one way is using the provided LocalPersist class, which is very easy to use.

Note: At the moment it only works for Android/iOS, not for the web.

First, import it:

import 'package:async_redux/local_persist.dart';

You need to convert yourself your objects to a list of simple objects composed only of numbers, booleans, strings, lists and maps (you can nest lists and maps).

For example, this is a list of simple objects:

List<Object> simpleObjs = [
'Goodbye',
'"Life is what happens\n\rwhen you\'re busy making other plans." -John Lennon',
[100, 200, {"name": "João"}],
true,
42,
];

Then create a LocalPersist class to use the /db/myFile.db file:

var persist = LocalPersist("myFile");

You can save the list to the file:

await persist.save(simpleObjs);

And then later load these objects:

List<Object> simpleObjs = await persistence.load();

Usually the save method rewrites the file. But it also lets you append more objects:

List<Object> moreObjs = ['Lets', 'append', 'more'];
await persist.save(simpleObjs, append: true);

You can also delete the file, get its length, see if it exists etc:

int length = await persist.length();
bool exists = await persist.exists();
await persist.delete();

Note: LocalPersist uses a file format similar to JSON, but not exactly JSON, because JSON cannot be appended to. If you want to save and load a single object into standard JSON, use the saveJson and loadJson methods:

await persist.saveJson(simpleObj);
Object? simpleObj = await persistence.loadJson();

Have a look at the: Local Persist tests.