DEV Community

Chris Sainty
Chris Sainty

Posted on • Originally published at chrissainty.com on

Mobile Blazor Bindings - State Management and Data

Mobile Blazor Bindings - State Management and Data


Mobile Blazor Bindings for Web Developers Series

Part 1 - Mobile Blazor Bindings - Getting Started

Part 2 - Mobile Blazor Bindings - Layout and Styling

Part 3 - Mobile Blazor Bindings - State Management and Data (this post)


Last time, we looked at layout and styling options for Mobile Blazor Bindings (MBB). We learned about the various page types and layout components available to us, as well as how we could style our components using either parameter styling or the more familiar CSS approach. We finished up by applying what we learned to our Budget Tracker app, adding various layout components and styling to make the app look a bit more appealing.

In this post, we're going to explore state management and data in MBB. We'll look at some option to manage the state of our applications ranging from simple to more complex. Then we'll talk about data, specifically, how to persist it. We'll cover how to deal with local persistence as well as persisting back to a server. Just as before, we'll finish up by applying what we've learned to our Budget Tracker app.

State Management

Just as with web applications, we need to be able to manage the state of our mobile apps. Even in a simple app like Budget Tracker, we have various bits of state to keep track of. The current budget, the current balance and the expenses that have been entered. Let's explore a couple of options we could use.

Storing state in components

The simplest thing we can do when it comes to state is to manage it within a component. The Budget Tracker at the end of the last post stores and manages its state this way – all state is kept on the HomePage component.

@code {

    private decimal _budget;
    private List<Expense> _expenses = new List<Expense>();

    private decimal _currentBalance => _budget - _expenses.Sum(x => x.Amount);
    private decimal _expensesTotal => _expenses.Sum(x => x.Amount);

}

The other components in the app update these values using Blazors EventCallback approach.

This method works really well in simple scenarios such as the Budget Tracker app where there is only a single page component. But, depending on the app, in multi page apps this stops being a good option and we would need to look at something a bit more advanced.

AppState class

The next level up would be implementing an AppState class. We can record all the bits of state we need to keep track of across our application in this one place. This class is registered with the DI container as a Singleton, giving all components we inject it into access to the same data.

Say for example we had an ecommerce application. We could use an AppState class to keep track of the contents of the shopping basket. It might look something like this.

public class AppState
{
    private readonly List<Item> _basket = new List<Item>();
    public IReadOnlyList<Item> Basket => _basket.AsReadOnly();

    public event Action OnChange;

    public void AddItem(Item newItem)
    {
        _basket.Add(newItem);
        NotifyStateChanged();
    }

    public void RemoveItem(Item item)
    {
        _basket.Remove(item);
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}

You'll notice that the Basket property is read only, this is important as we wouldn't want it being changed randomly. We want changes to go through the public methods, AddItem and RemoveItem, so we can ensure the OnChange event is raised. All the components in our app that care about the state of the basket can subscribe to this event and be notified when updates happen.

@inject AppState AppState
@implements IDisposable

<StackLayout>
    <Label Text="@($"{AppState.Basket.Count} Items in Basket")" />
</StackLayout>

@code {

    protected override void OnInitialized()
    {
        AppState.OnChange += StateHasChanged;
    }

    public void Dispose() => AppState.OnChange -= StateHasChanged;
}

In the code above, whenever an item is added or removed from the basket the OnChange event will trigger the component to re-render by calling StateHasChanged. We're also using implementing IDisposable so we can safely unsubscribe from the OnChange event when the component is destroyed.

This method works really well and is pretty simple to implement and get going with. However, you may find it doesn't work well in large applications. The more state that's tracked the bigger the class gets as we add more methods to update the various values. Eventually, it will become quite difficult to navigate and maintain.

At this point it would be worth looking at breaking the state down into smaller chunks, alternatively, you could also look at doing a Redux or MobX implementation for you MBB app. There some example out there of implementing Redux in a Xamarin Forms app which should be fairly easy to port to MBB. I think the Fluxor library from Peter Morris would also work, although I've not tested it myself.

Data

Let's talk about what we can do with our data. In mobile apps we have three scenarios we potentially need to cater for. Online, offline and a mix of both.

Online - Saving data to an API

Saving data back to an API in MBB isn't much different to what we would do in a web based Blazor app, which is handy.

There isn't an HttpClient configured out of the box, so we need to install the Microsoft.Extensions.Http NuGet package. Once installed we add the following line to register the various services with the DI container.

.ConfigureServices((hostContext, services) =>
{
    // Register app-specific services
    services.AddHttpClient();
})

We can also install the same HttpClient helper methods we're used to from Blazor by adding the System.Net.Http.Json package. This is currently a pre-release package, if you're not familiar with it you can read more about it in this blog post. We now have access to GetFromJsonAsync, PostAsJsonAsync and PutAsJsonAsync.

We can now inject an IHttpClientFactory into any component and use the CreateClient method to get an instance of the HttpClient to make our API calls with. From here things are the same as they would be in a web based Blazor app.

Offline - Storing data on the device

The most common option for storing data locally on the device is SQLite. It's a small, fast, self-contained, high-reliability, full-featured, SQL database engine – it's also free and open source.

Using SQLite is really easy. Once the sqlite-net-pcl NuGet package is installed, we need to define entity classes, similar to how we would with Entity Framework. These represent the shape of the data we're going to persist. For example, if we were saving cars, a Car entity could be defined like this.

public class Car
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public int Doors { get; set; }
}

This is a simple POCO (Plain Old CLR Object/Plain Old C# Object) class the only bit of magic that's been added is the PrimaryKey and AutoIncrement attributes. These mark the primary key for the table and ensure it has an auto-incrementing value.

Then we need to define a database class. this performs a few jobs. It ensures that the database is created along with any tables, it's also where we define any methods for saving or retrieving data. You can think of it as a mix between an Entity Framework DB context and a repository.

public class Database
{
    readonly SQLiteAsyncConnection _database;

    public Database(string dbPath)
    {
        _database = new SQLiteAsyncConnection(dbPath);
        _database.CreateTableAsync<Car>().Wait();
    }

    public Task<List<Car>> GetCarsAsync()
    {
        return _database.Table<Car>().ToListAsync();
    }

    public Task<int> SaveCarAsync(Car newCar)
    {
        return _database.InsertAsync(newCar);
    }
}

The final job is to add an instance of the Database class into the DI container so we can using it by injecting it into our components.

public App()
{
    var database = new Database(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "cars.db3"));

    var host = MobileBlazorBindingsHost.CreateDefaultBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            // Register app-specific services
            services.AddSingleton<Database>(database);
        })
        .Build();

        // other code omitted
}

That's all there is to it, data can now be saved locally on the device and will persist between app restarts.

Both - Catering for offline scenarios

The final option we'll look at is a mix of online and offline. This is probably the majority of mobile apps. And really this is about knowing if we're online or offline. If we're online we can make an API and if we're offline we can save data to a local database until the network is back and then push it up to the API. Now, we could just make API calls and see if they time out, then if they do, save locally. But this doesn't seem very elegant. Luckily for us there is a much simpler way thanks to Xamarin Essentials.

We'll talk more about Xamarin Essentials another time but it's a great library which gives us access to loads of OS and platform APIs from C#, one of which is the Connectivity class. This gives us a simple API we can call to determine the network status of the device.

var current = Connectivity.NetworkAccess;

if (current == NetworkAccess.Internet)
{
    // Connection to internet is available
}

We can even subscribe to an event which will tell us when the network status changes.

public ConnectivityTest()
{
    // Register for connectivity changes, be sure to unsubscribe when finished
    Connectivity.ConnectivityChanged += HandleConnectivityChanged;
}

void HandleConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
{
    // Do something based on new network status
}

This gives us everything we need to be able to cater for scenarios where the app may not have a network connection.

Adding State Management & Data to Budget Tracker

Let's put everything together and apply it to Budget Tracker. Now, as I mentioned earlier, Budget Tracker can work perfectly fine with storing all its state on the HomePage component. But using an AppState class would clean up some of the code keeping everything in sync. Another advantage would come when persisting data. We aren't going to add an API to this project, that seems a bit overkill. But it would be great to be able to save data locally using SQLite. If we added an AppState class we could do this much easier as everything is in one place.

Adding an AppState Class

We'll start by defining an AppState class, in the root of the project, which contains all of the state which was originally kept in the HomePage component.

public class AppState
{
    private readonly List<Expense> _expenses = new List<Expense>();

    public decimal Budget { get; private set; }
    public IReadOnlyList<Expense> Expenses => _expenses.AsReadOnly();
    public decimal CurrentBalance => Budget - _expenses.Sum(x => x.Amount);
    public decimal ExpensesTotal => _expenses.Sum(x => x.Amount);

    public event Action OnChange;

    public void SetBudget(decimal newBudget)
    {
        Budget = newBudget;
        NotifyStateChanged();
    }

    public void AddExpense(Expense newExpense)
    {
        _expenses.Add(newExpense);
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}

We're also going to register it as a singleton in the DI container in App.cs

var host = MobileBlazorBindingsHost.CreateDefaultBuilder()
    .ConfigureServices((hostContext, services) =>
    {
        // Register app-specific services
        services.AddSingleton<AppState>();
    })
    .Build();

With those bits in place it's just a case of updating the various components to pull their state directly from the AppState class. Here's what the updated HomePage component looks like.

@inject AppState AppState
@implements IDisposable

<StyleSheet Resource="BudgetTracker.css" Assembly="GetType().Assembly" />

<StackLayout class="homeContainer">

    <Frame>
        <StackLayout>
            @if (AppState.Budget > 0)
            {
                <BudgetSummary />
            }
            else
            {
                <SetBudget />
            }
        </StackLayout>
    </Frame>

    @if (AppState.Budget > 0)
    {
        <Frame>
            <ScrollView>
                <StackLayout>
                    <Label Text="EXPENSES" />
                    <ExpenseList />
                    <CreateExpense />
                </StackLayout>
            </ScrollView>
        </Frame>
    }

</StackLayout>

@code {

    protected override void OnInitialized() => AppState.OnChange += StateHasChanged;

    public void Dispose() => AppState.OnChange -= StateHasChanged;
}

You can view all of the changes to the app over on the GitHub repo.

Storing Data via SQLite

Great! We now have the state of the app in a central place. Next we're going to add in SQLite so we can save the values we enter between app reboots. First we need to define our database class.

public class BudgetTrackerDb
{
    private readonly SQLiteAsyncConnection _database;

    public BudgetTrackerDb(string dbPath)
    {
        _database = new SQLiteAsyncConnection(dbPath);
        _database.CreateTableAsync<Budget>().Wait();
        _database.CreateTableAsync<Expense>().Wait();
    }

    public async Task<int> SaveBudgetAsync(Budget newBudget)
    {
        var result = await _database.InsertAsync(newBudget);

        return result;
    }

    public async Task<decimal> GetBudgetAsync()
    {
        // This is nasty but as we're only going to have one budget for now so we'll let it slide
        var result = await _database.Table<Budget>().FirstOrDefaultAsync(x => x.Amount > 0);

        return result?.Amount ?? 0;
    }
    public async Task<int> SaveExpenseAsync(Expense newExpense)
    {
        var result = await _database.InsertAsync(newExpense);

        return result;
    }

    public Task<List<Expense>> GetExpensesAsync()
    {
        return _database.Table<Expense>().ToListAsync();
    }

}

Our DB has two tables, one to store the budget and one to store the expenses. This setup would also allow us to expand the functionality of the app at a later data and support multiple budgets. We've also defined a few methods to save and retrieve data from the database.

With the database class in place, next we're going to update our AppState class.

public class AppState
{
    private readonly BudgetTrackerDb _budgetTrackerDb;

    public AppState()
    {
        _budgetTrackerDb = new BudgetTrackerDb(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "BudgetTrackerDb.db3"));
    }

    public event Func<Task> OnChange;

    public async Task SetBudget(decimal newBudget)
    {
        _ = await _budgetTrackerDb.SaveBudgetAsync(new Budget { Amount = newBudget });
        await NotifyStateChanged();
    }

    public async Task<decimal> GetBudget() => await _budgetTrackerDb.GetBudgetAsync();

    public async Task AddExpense(Expense newExpense)
    {
        _ = await _budgetTrackerDb.SaveExpenseAsync(newExpense);
        await NotifyStateChanged();
    }

    public async Task<IReadOnlyList<Expense>> GetExpenses() => await _budgetTrackerDb.GetExpensesAsync();

    public async Task<decimal> GetCurrentBalance()
    {
        var budget = await GetBudget();
        var expenses = await GetExpenses();

        return budget - expenses.Sum(x => x.Amount);
    }

    public async Task<decimal> GetTotalExpenses()
    {
        var expenses = await GetExpenses();

        return expenses.Sum(x => x.Amount);
    }

    private async Task NotifyStateChanged() => await OnChange?.Invoke();
}

Essentially what we've done here is written methods which retrieve the data from our database rather than just storing the values in memory. An advantage of doing this is if our app needed to support online and offline functionality. We could easily add in a check to see if the app is connected or not, if it isn't then we could save locally to the SQLite database. But if it was then we could use a HttpClient to send and retrieve data from an API. But the rest of our app wouldn't need to care, it could all be handled inside the AppState class.

The final job now is to make some updates to the various components to work with the new methods. Here is the final HomePage component.

@inject AppState AppState
@implements IDisposable

<StyleSheet Resource="BudgetTracker.css" Assembly="GetType().Assembly" />

<StackLayout class="homeContainer">

    <Frame>
        <StackLayout>
            @if (budgetSet)
            {
                <BudgetSummary />
            }
            else
            {
                <SetBudget />
            }
        </StackLayout>
    </Frame>

    @if (budgetSet)
    {
        <Frame>
            <ScrollView>
                <StackLayout>
                    <Label Text="EXPENSES" />
                    <ExpenseList />
                    <CreateExpense />
                </StackLayout>
            </ScrollView>
        </Frame>
    }

</StackLayout>

@code {

    private bool budgetSet;

    protected override async Task OnInitializedAsync()
    {
        await UpdateState();
        AppState.OnChange += UpdateState;
    }

    public void Dispose() => AppState.OnChange -= UpdateState;

    private async Task UpdateState()
    {
        var budget = await AppState.GetBudget();
        budgetSet = budget > 0;

        StateHasChanged();
    }
}

That's it, we're done. You can see the full updated source code on the GitHub repo.

Summary

In this post we've looked at some options around managing state and storing data in a Mobile Blazor Bindings application.

Starting with state, we looked at the most simple options first, state in components. We then moved on to a more advanced method using a central class called AppState to store all state in the application.

Next we looked at data and how we can store it locally on the device using SQLite, a popular, open source, portable SQL database. We talked about how we could configure MBB to make API calls using the HttpClient so we could persist data back to the server. We also covered how we could use the Xamarin Essentials Connectivity class to determine if the app is connected in order to decide if data should be saved locally or an API call should be attempted.

Finally, we applied some of our learnings to the Budget Tracker application. We added an AppState class to hold all of the app state centrally. We also added in SQLite to store our budget and any expenses we'd added.

Top comments (0)