Omnitalented

Converting My Contacts: Step 1. SetttingsPage, Middleware

June 09, 2020

I’m writing a series of posts on converting My Contacts app using Laconic.

Laconic can be used to write entire apps, but it was designed to fit into existing Xamarin.Forms code. You can start small: replace a view, replace a page. To demonstrate how it can be done I’ve decided to take a full-featured sample app from Xamarin, and convert it to using Laconic, step by step.

By the series finale, we’re going to have all XAML removed, all of the MVVM-mandated ceremony eliminated. The result is going to be a cleaner architecture and cleaner code while keeping the functionality the same.

Step 1: SettingsPage

We are going to start with the Settings page:


Converting the UI code is pretty straightforward. A big part of the job can be done with a bunch of search/replace. This:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Label
        Margin="10"
        Style="{DynamicResource LargeLabelStyle}"
        Text="Settings"
        VerticalOptions="Center"
        HorizontalOptions="{OnPlatform iOS=Center, Default=Start}"
        Grid.ColumnSpan="2"/>
    <Button
        .../>

becomes this:

new Grid {
    RowDefinitions = "Auto, *",
    ColumnDefinitions = "*, Auto",
    ["title", columnSpan: 2] = new LargeLabel(state)
    {
        Text = "Settings",
        Margin = 10,
        VerticalOptions = xf.LayoutOptions.Center,
        HorizontalOptions = xf.LayoutOptions.Center,
    },
    ["closeButton", column: 1] = new Button
    {...
    }

One small observation: in the original SettingsPage.xaml there’s quite a bit of copy/pasted XAML with only the value of the BackgroundColor attribute changing:

<BoxView
    BackgroundColor="{DynamicResource SystemBlue}"
    HeightRequest="20"
    HorizontalOptions="FillAndExpand" />
<BoxView
    BackgroundColor="{DynamicResource SystemGreen}"
    HeightRequest="20"
    HorizontalOptions="FillAndExpand" />
... Seven more BoxView elements

We feel your pain, James. Encapsulating bits with XAML is quite a chore. For a reusable view you have to have a separate .xaml file with the accompanying .xaml.cs, and write plenty of boilerplate code for bindable properties. While with Laconic, since it’s plain C#, the task is trivial:

static BoxView ColorRow(xf.Color color) => new BoxView
{
    BackgroundColor = color, HeightRequest = 20, HorizontalOptions = xf.LayoutOptions.FillAndExpand
};

There are two new files:

State.cs contains our new application state classes, and a signal class used to notify that the user changed the app theme. Our state is immutable, and the main reducer is a pure function.

Components.cs contains blueprint classes which are reused throughout the UI. They replace a few styles in App.xaml. These classes are purish, they don’t operate on anything but the values passed to their constructors.

Remember the diagram from Introduction to Laconic? Laconic gently pushes us towards using pure functions. The benefits are numerous: the flow is straightforward; composing a UI from smaller bits is easier, since there are no side effects; unit testing is super simple.

And here we’re facing the first challenge: No app code lives in a pure blue bubble. An app needs to make network requests, read files, handle device notifications. Activities like that are inherently impure. The app we’re converting uses DynamicResource bindings throughout, and those resource values are changed from the Settings page.

What are we going to do here? The answer is Laconic’s middleware.

Middleware

Middleware support in Laconic is a way for an app developer to plug into the Signal -> Reducer -> Blueprint Maker cycle. It’s modeled after ASP.NET Core middleware, and should be instantly familiar to most Xamarin.Forms developers.

A middleware is a function that takes context and a reference to the next middleware in the pipeline and returns context. The more complete picture of the execution flow with Laconic looks like this:

Much like ASP.NET Core middleware, Laconic’s middleware can run code before and/or after the next middleware in the pipeline, or it can short-circuit the evaluation of other steps. The middleware can inspect the state and can modify it.

The main reducer is at the end of the middleware pipeline. Think of it as the code inside of ASP.NET Core Controller’s Action.

While reducers and blueprint makers should be pure, it’s OK for middleware to be not. Actually, this is the intended use. Use it for logging, for receiving updates from the device timer, etc.

Luckily enough, in the original My Contacts app, all the functionality we need in our new Settings page is encapsulated in a couple of static methods. We just need to wrap it in our middleware:

_binder.UseMiddleware((context, next) =>
{
    context = next(context);
    
    if (context.Signal is SetThemeSignal t)
    {
        Settings.ThemeOption = context.State.Themes.Length == 3 ? t.Payload : t.Payload + 1;
        ThemeHelper.ChangeTheme(t.Payload);

        return context.WithState( new State (
            new Colors(xf.Application.Current.Resources),
            (
                xf.Device.GetNamedSize(Xamarin.Forms.NamedSize.Large, typeof(Label)),
                xf.Device.GetNamedSize(Xamarin.Forms.NamedSize.Large, typeof(Label))
            ),
            context.State.Themes,
            context.State.SelectedTheme)
        );
    }

    return context;
});

And dismissing modal SettingsPage is a s simple as:

_binder.UseMiddleware((context, next) =>
{
    if (context.Signal.Payload = "closeSettings")
        Navigation.PopModalAsync();

    return next(context);
});

Summary

Let’s calculate some stats.

Before:

------------------------------------------------------------------
Language        files          blank        comment           code
------------------------------------------------------------------
C#                 51            435            250           1952
XAML                9            103             19            922
MSBuild script      6             13              7            597
XML                12             20             32            476
JSON                4              0              0            157
Bourne Shell        3              6              2             17
------------------------------------------------------------------
SUM:               85            577            310           4121
------------------------------------------------------------------

Character count, XAML and C# files: 130,047

After:

------------------------------------------------------------------
Language        files          blank        comment           code
------------------------------------------------------------------
C#                 52            450            252           2120
XAML                8             96             19            813
MSBuild script      6             11              7            578
XML                12             20             32            476
JSON                4              0              0            157
Bourne Shell        3              6              2             17
------------------------------------------------------------------
SUM:               85            583            312           4161
------------------------------------------------------------------

Character count, XAML and C# files: 132,307

Well, we have an increase in numbers… Laconic isn’t being laconic, does it? Don’t worry. We had to lay down the initial infrastructure and we’re going to see a dramatic decrease in the amount of code with later stages of the conversion. Look at the bright side: one XAML file and one view model were eliminated, yay!

I would argue that the new code is easier to understand and to maintain. We now have an immutable state, we have pure functions for modifying it. The functions for creating the UI are pure too. There’s no data binding, no indirection. Since on every change of the state the code creates an entire blueprint (virtual view) tree everything can be read top-down, starting with MainContent function.

Check out the the code for Step 1 of the conversion: https://github.com/shirshov/app-contacts/tree/step1-settings-page Don’t forget to do git submodule init && git submodule update