Advanced Redux in Xamarin Part 2: Persistent Actions Middleware
Warning: Please consider that this post is over 6 years old and the content may no longer be relevant.
In this second post in the series on advanced Redux in Xamarin, we’ll look at how to persist Actions so application state can be restored when the app restarts. We’ll do this by creating Middleware that will intercept each action and save it to a persistent store, then replay these Actions on app startup to restore our state.
Middleware
Middleware is a composable chain of custom code where you can manipulate actions before they are received by the reducers. You can see how the middleware pattern is implemented in Redux.NET here
private Dispatcher ApplyMiddlewares(params Middleware<TState>[] middlewares)
{
Dispatcher dispatcher = InnerDispatch;
foreach (var middleware in middlewares)
{
dispatcher = middleware(this)(dispatcher);
}
return dispatcher;
}
The InnerDispatch
is a method that takes an action and passes it to the Reducers, the ApplyMiddlewares
method chains any custom Middleware you provide together, before InnerDispatch gets called.
A Redux.NET Middleware is (from the docs):
A function that, when called with a Dispatcher, returns a new Dispatcher that wraps the first one.
A simple Middleware that appends an incrementing id to every action might look like this
public class UniqueIdMiddleware
{
private int nextId = 0;
public Middleware<TState> CreateMiddleware<TState>()
{
return store => next => action =>
{
if (action is IUniqueIdAction)
{
((IUniqueIdAction)action).UniqueId = nextId++;
}
return next(action);
};
}
}
And we wire it up by passing each Middleware to the store constructor in App.xaml.cs
Store = new Store<ApplicationState>(
Reducers.ReduceApplication,
new ApplicationState(),
new UniqueIdMiddleware().CreateMiddleware<ApplicationState>());
Persistent Actions Middleware
We want to write Middleware that will save all Actions to a persistent data store, then rehydrate all those Actions back into State when we restart the app. There are already a couple of implementations of this for the JavaScript version of Redux, and fortunately for us, the author of Redux.NET has already provided us with reference .NET implementation
public class LiteDbMiddleware {
public static Middleware<TState> CreateMiddleware<TState>(String databaseName) {
var db = new LiteDatabase(databaseName);
var continuum = db.GetCollection<PointInTime<TState>>("spacetime");
return store => {
var pointsInTime = continuum.FindAll().ToList();
pointsInTime.ForEach(point => store.Dispatch(point.LastAction));
return next => action => {
if (action is GetHistoryAction) {
return continuum.FindAll().ToList();
}
var result = next(action);
continuum.Insert(new PointInTime<TState> { LastAction = action, State = store.GetState() });
return result;
};
};
}
public static IEnumerable<PointInTime<TState>> GetHistory<TState>(Dispatcher dispatch) {
return dispatch(new GetHistoryAction()) as IEnumerable<PointInTime<TState>>;
}
}
This uses LiteDb to persist all the actions and dispatches all actions again when the store is first initialised. This example doesn’t work as illustrated, so we’re going to need to modify it a bit.
Persist Actions
First, we’ll store all actions into a LiteDb database. We need to create an ActionHistory
wrapper because LiteDb expects all objects to have an Id
property.
public class PersistActionsMiddleware
{
public static Middleware<TState> CreateMiddleware<TState>(string databaseName)
{
var db = new LiteDatabase(databaseName);
var actionCollection = db.GetCollection<ActionHistory>("ActionHistory");
return store =>
{
return next => action =>
{
var result = next(action);
actionCollection.Insert(new ActionHistory { Action = action });
return result;
};
};
}
}
public class ActionHistory
{
public int Id { get; set; }
public IAction Action { get; set; }
}
This is pretty much a stripped down version of the sample code. We need to register this where we create our store (usually App.xaml.cs
)
var dbPath = DependencyService.Get<IFileHelper>().GetLocalFilePath("todo.db");
Store = new Store<ApplicationState>(
Reducers.Reducers.ReduceApplication,
new ApplicationState(),
LiteDbMiddleware.CreateMiddleware<ApplicationState>(dbPath));
Replay Actions
Our PersistActionsMiddleware will now store all Actions, but we want to replay the Actions when the app starts up. To do this we’ll add a ReplayHistory()
method and give our class some instance level variables, so CreateMiddleware
will no longer be static.
public class PersistActionsMiddleware<TState>
{
private IStore<TState> _store;
private readonly LiteCollection<ActionHistory> _actionCollection;
public PersistActionsMiddleware(String databaseName)
{
var db = new LiteDatabase(databaseName);
_actionCollection = db.GetCollection<ActionHistory>("ActionHistory");
}
public Middleware<TState> CreateMiddleware()
{
return store =>
{
_store = store;
return next => action =>
{
var result = next(action);
_actionCollection.Insert(new ActionHistory { Action = action });
return result;
};
};
}
public void ReplayHistory()
{
foreach (var actionHistory in _actionCollection.FindAll())
{
_store.Dispatch(actionHistory.Action);
}
}
}
We’ll call ReplayHistory()
after we’ve created the store.
var dbPath = DependencyService.Get<IFileHelper>().GetLocalFilePath("todo.db");
var liteDbMiddleware = new LiteDbMiddleware<ApplicationState>(dbPath);
Store = new Store<ApplicationState>(
Reducers.Reducers.ReduceApplication,
new ApplicationState(),
liteDbMiddleware.CreateMiddleware());
liteDbMiddleware.ReplayHistory();
“But our sample implementation was so much cleaner!” Yep, it would be great to be able to dispatch actions when we’re setting up our middleware like this
return store => {
var pointsInTime = continuum.FindAll().ToList();
pointsInTime.ForEach(point => store.Dispatch(point.LastAction));
return next => action => {
But unfortunately, this won’t work with the current implementation of Redux.NET. To setup the Dispatcher
on the store, Redux.NET is calling our CreateMiddleware
function, so when we try to dispatch an action in our middleware creation, store.Dispatch
is null.
Avoid persisting the replayed Actions
So we can now replay our Actions. But there’s a big problem! Every time we dispatch a saved Action, the PersistActionsMiddleware saves it again to our database. Let’s tell it not to save Actions while we’re replaying the history by introducing a _isReplaying field.
public class PersistActionsMiddleware<TState>
{
private IStore<TState> _store;
private readonly LiteCollection<ActionHistory> _actionCollection;
private bool _isReplaying;
public PersistActionsMiddleware(String databaseName)
{
var db = new LiteDatabase(databaseName);
_actionCollection = db.GetCollection<ActionHistory>("ActionHistory");
}
public Middleware<TState> CreateMiddleware()
{
return store =>
{
_store = store;
return next => action =>
{
var result = next(action);
if (_isReplaying) return result;
_actionCollection.Insert(new ActionHistory { Action = action });
return result;
};
};
}
public void ReplayHistory()
{
_isReplaying = true;
foreach (var actionHistory in _actionCollection.FindAll())
{
_store.Dispatch(actionHistory.Action);
}
_isReplaying = false;
}
}
We now have a working implementation of PersistActionsMiddleware.
Warning: Don’t do this!
After looking at how we’d save all Actions to a persistent store, I would caution you not to do this in a Xamarin app, or at least think carefully about it. Redux Actions are intended to be raised for many UI changes and user input, most have no relevance (other than analytics) once performed, and storing all these to the user’s device is going to consume more and more device storage over time.
Using persistent actions to sync with the server
At PageUp we’re using persistent Actions Middleware to record our Actions and send them to the server at regular intervals in order to synchronise our application state with the server. The server then replies with a list of actions that have occurred on the server and we dispatch them on the app to update our local state. Once we’ve successfully synced with the server we clear out the stored list of Actions and start recording them again.
So how should we record the state of the app without an ever growing Action stream? In the final post in this series, we’ll look at writing Middleware to store the current application state to a local database and reloading that state at startup.