Automatically persist and restore application state in ReactiveUI

Automatically persist and restore application state in ReactiveUI

TLDR; SuspensionHost helps by saving and loading your app state when your app gets suspended. Example code is on GitHub.


ReactiveUI has a bunch of really cool features. Even when you are already using it, you sometimes stumble over some lesser known capabilities. This is what happened to me when I first read about the suspension feature. Join me on my beginners guide to this amazing helper.

What is ReactiveUI?

If you have never heard of ReactiveUI, let me quickly introduce you to it. ReactiveUI is a opinionated framework for the Model-View-ViewModel pattern for C#. It makes use of a lot of reactive features, like observables, hence the name. One of the major selling point for ReactiveUI is it's amazing support for a lot of different platforms. It supports Xamarin Forms, WPF, Avalonia, Uno, MAUI and even Blazor. By using ReactiveUI you can develop real cross platform apps and share a lot of logic between the different platforms.

What problem does SuspensionHost solve?

Especially in the mobile world, we never really know when our application will be paused, closed or resumed. Often this process is highly optimized by the operating system. But to make sure, that our user has the best possible experience, we don't want him to loose all of his state. Imagine you open some app where you have to insert your contact details and in the middle of this process you receive a call. When the app gets suspended in the background, you might loose all of the data you already entered. This is not a great user experience. To fix this, we need to observe the application state and save the state, when our app gets closed and load it again when it gets resumed. As a cross platform developer you are responsible to handle this correctly. Different platforms have different lifecycles which makes this pretty complicated. Here comes ReactiveUI to the rescue! It's AutoSuspensionHelper class let's you pretty much automate this whole process of saving and loading your application state. Let's see how this is done in the next chapter.

How it works

Define our state

First, let's define a simple state class:


[DataContract]
public class MainState
{
    [DataMember]
    public string FirstName { get; set; } = string.Empty;

    [DataMember]
    public string LastName { get; set; } = string.Empty;
}

This is as simple as it gets. We just imagine, we have a contact form and we want to remember the state of this form. Take note how we annotate our class with the [DataContract] Attribute and our properties with the [DataMember]attribute. Using this attributes we can define which properties will be saved and which will be omitted. If we would like to omit a property, we can use the [IgnoreDataMember] attribute.

Listen to lifecycle events

Now let's have a look how we can automatically save and load our new application state. ReactiveUI provides us with an AutoSuspendHelper class. This class is available for all of the supported platforms. In this example I will showcase a .NET MAUI application. The AutoSuspendHelper class must know a few things:

  1. How can I create the state, if there is none yet?
  2. How (and where) should I save and load the state?
  3. When does the platform suspend or resume the app?

In our App.xaml.cs file we can provide the AutoSuspendHelper with all this information:

// App.xaml.cs
private readonly AutoSuspendHelper _suspension;

    public App()
    {
        InitializeComponent();
        // Create a new AutoSuspendHelper
        _suspension = new AutoSuspendHelper();
        // Tell it how to create new state
        RxApp.SuspensionHost.CreateNewAppState = () => new MainState();
        // Tell it how to save and load the data
        RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver(
            Path.Combine(FileSystem.AppDataDirectory, "state.json")));

        _suspension.OnCreate();

        // Load or create the state
        var state = RxApp.SuspensionHost.GetAppState<MainState>();
        var viewModel = new MainViewModel(state);

        MainPage = new MainPage
        {
            BindingContext = viewModel
        };
    }

    // Tell it when the platform lifecycle suspends or resumes the app
    protected override void OnResume()
    {
        base.OnResume();
        _suspension.OnResume();
    }

    protected override void OnStart()
    {
        base.OnStart();
        _suspension.OnStart();
    }

    protected override void OnSleep()
    {
        base.OnSleep();
        _suspension.OnSleep();
    }

Let's break this code down. First, we create a new AutoSuspendHelper and save it into a private field. Next we set ReactiveUIs SuspensionHost how to create new state. In our case we just want to create a new instance of the MainState class. Then we call the SetupDefaultSuspendResume method. Here we can pass a so called SuspensionDriver. In our case we pass it a NewtonsoftJsonSuspensionDriver. We will have a look at this class shortly. Last but not least we have to tell the suspension host when our platform lifecycle events happen. To achieve this we simply override these lifecycle events and call the matching method on the suspension object. This will look slightly different on different platforms, depending on which lifecycle methods there are.

Define how we store and load our state

The last unknown part of this guide is how we really store our state. In the last section we just passed a NewtonsoftJsonSuspensionDriver to our host. Let's have a look what this is and how it works. A suspension driver is just a class that implements the ISuspensionDriver interface. This means it is completely our choice how we would like to persist our data. In this case, as you might have guessed, we serialize our state as JSON and save it on the file system. To implement the ISuspensionDriver interface we need to implement three methods:

// NewtonsoftJsonSuspensionDriver.cs

public class NewtonsoftJsonSuspensionDriver : ISuspensionDriver
{
    private readonly string _stateFilePath;

    // Specify how the object is serialized
    private readonly JsonSerializerSettings _settings = new()
    {
        TypeNameHandling = TypeNameHandling.All,
        NullValueHandling = NullValueHandling.Ignore,
        ObjectCreationHandling = ObjectCreationHandling.Replace,
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
    };

    // The location of the file is passed in the constructor
    public NewtonsoftJsonSuspensionDriver(string stateFilePath)
    {
        _stateFilePath = stateFilePath;
    }

    // load our state from the disk and deserialize it
    public IObservable<object> LoadState()
    {
        if (!File.Exists(_stateFilePath))
        {
            return Observable.Throw<object>(new FileNotFoundException(_stateFilePath));
        }

        var content = File.ReadAllText(_stateFilePath);
        var state = JsonConvert.DeserializeObject<object>(content, _settings);
        return Observable.Return(state!);
    }

    // serialize our state and save it on disk
    public IObservable<Unit> SaveState(object state)
    {
        var lines = JsonConvert.SerializeObject(state, Formatting.Indented, _settings);
        File.WriteAllText(_stateFilePath, lines);
        return Observable.Return(Unit.Default);
    }

    // our state is no longer valid -> we delete the file
    public IObservable<Unit> InvalidateState()
    {
        if (File.Exists(_stateFilePath))
            File.Delete(_stateFilePath);
        return Observable.Return(Unit.Default);
    }
}

The code in this file is pretty self explanatory. We receive the location of our file in the constructor (in this case we are using MAUI Essentials to get the path to the app directory), and in the respective methods we either save or load our state from disk.

You might wonder why I am using Newtonsoft.Json instead of the builtin System.Text.Json. The reason is a feature that we are using here, that System.Text.Json is missing right now: TypeNameHandling. We are setting the TypeNameHandling to All in the JsonSerializerSettings. This means, that Newtonsoft will remember the state of the object it is serializing and is able to deserialize it later. When we have a look at the resulting json file we see the full type name of our MainState class in the file. If we wouldn't have this, the deserializer would have no idea which object it should create.

{
  "$type": "SuspensionHostDemo.Presentation.State.MainState, SuspensionHostDemo.Presentation",
  "firstName": "Sören",
  "lastName": "Christ"
}

Using our state

Our state object is pretty useless if we don't use it. In this case we keep it pretty simple. Have a look at the App.xaml.cs file again. There we get a instance of our state object by calling var state = RxApp.SuspensionHost.GetAppState<MainState>(). This tells our SuspensionHost that we want to load our state: if there already is persisted state, the driver will load it from the disk, if not the host will create a new instance. This state object is passed down to our view model which makes it available to our view to display or change it. To keep this post focused I will not show the code of the view model and the view. If you are interested check it on GitHub.

Running the application

Now it is time to run our example application. It doesn't matter on which platform you run the MAUI application (Android, iOS, Windows or Mac) as it should work on all of them! When we first start the app we just see two empty textboxes. One for our first name and one for the last name. Let's enter some data. After that just close the application. On Android and iOS make sure to also kill the app from the task manager to completely wipe it from memory. When we now start the application again: Voilà! Our form is still filled with the data that we provided before.

Feel free to add some breakpoints in your driver class to find out when the respective methods are called.

Wrapping it up

As you can see, ReactiveUI makes it pretty easy to automatically save and load your application state. The suspension driver using Newtonsoft.Json is just one possibility. ReactiveUI shows a driver using Akavache on their documentation page so make sure to check it out. In the GitHub repository there are example projects that showcase how to setup this feature for .NET MAUI, Avalonia or WPF. Feel free to clone it and play around.

Bonus (11/16/2022) ...

The ReactiveUI documentation page states, that you can use this feature not just to persist and load some arbitrary state class, like we have done here. They say, that you can easily persist your complete view model including the ReactiveUI navigation stack. As of today, this feature is currently broken. Check the GitHub issue here. The issue also provides a workaround!