Omnitalented

Converting My Contacts: Step 3. Local state, records

August 23, 2020

This article is a part of a series on converting My Contacts app using Laconic. The start of the series is here.

Step 3: Contact Editor Page

In this step we convert the page where the user can edit a contact:



If you were following this series from the beginning, then you probably know what the outcome is going to be: all the UI code is now in C#. No more XAML, no more ViewModel with associated baggage like Data Binding, ValueConverters, Styles, etc. Everything is in pure static functions, with simple to understand top-down execution flow.

Note: to keep this step simpler the validation of the user input is removed. It will come back in later steps.

Code Generation for Records and Signals

I’m developing Laconic as an attempt to bring functional programming model to Xamarin.Forms, as much as possible. One of the principles of FP is immutable state. Until we have C# 9 available, with its support for record, writing immutable classes in C# is quite a chore. This is where a sister project for Laconic, Laconic.CodeGeneration comes handy.

The entire state code for the app (except for Colors class) that we had to write manually now looks like this:

[Records]
public interface Records // The interface name doesn't matter
{
    record Contact(string id, string firstName, string lastName, 
        string company, string jobTitle, 
        string phone, string email,
        string street, string city, string state, string postalCode,
        string photoUrl);

    record NamedSizes(double large, double medium, double small, double micro);
    record Visuals(Colors colors,  NamedSizes sizes, string[] themes, Theme selectedTheme);
    record State(bool isFetchingData, Contact[] contacts, Visuals visuals);
}

Given this input Laconic.CodeGeneration generates quite a bit of code: classes with constructor, read only properties, value equality and With method. The latter is what allows us to keep our MainReducer very brief:

public static State MainReducer(State state, Signal signal) => signal switch {
    DataRequested _ => state.With(isFetchingData: true),
    DataReceived rec => state.With(isFetchingData: false, contacts: rec.Contacts.ToArray()),
    ThemeUpdated tu => state.With(visuals: state.Visuals.With(colors: tu.Colors, sizes: tu.NamedSizes)),
    _ => state,
};

And also, being a good sibling, Laconic.CodeGeneration has special support for generating Signals:

    [Signals]
    interface __AppContactsSignal
    {
        Signal DataRequested();
        Signal DataReceived(IEnumerable<Contact> contacts);
        Signal SaveContact(Contact contact);
        Signal SetTheme(Theme theme);
        Signal ThemeUpdated(Theme newTheme, Colors colors, NamedSizes namedSizes);
    } 

Of course, a Signal class can be written manually, but why bother? I’d better save my boilerplate for other occasions. Unfortunately, there are plenty of places where I have to use it… Compare the code for ThemeUpdated signal above with its equivalent written by hand:

public class ThemeUpdated : Signal<Theme>
{
    public Theme NewTheme { get; }
    public Colors Colors { get; }
    public NamedSizes NamedSizes { get; }

    public ThemeUpdated(Theme newTheme, Colors colors, NamedSizes namedSizes) : base(newTheme)
    {
        NewTheme = newTheme;
        Colors = colors;
        NamedSizes = namedSizes;
    }
}

LocalContext

Now that we have our nicely immutable state we have to decide where to keep the contact that is being edited. The code for the editor page is all in static methods, we can’t have any instance data there. We could put it in the app state:

record State(bool isFetchingData, 
    Contact[] contacts, 
    Visuals visuals, 
    Contact editingContact);

and when the user taps the Save button we update the main list and set editingContact to null. But that just doesn’t feel right… The transient, incomplete values of the user input should be kept in the scope of the ContactEditor page until the saving request.

For this use case Laconic has a concept of LocalContext. Instead of returning new ContentPage{...} directly our ContactEditor.Page method uses a static method Element.WithContext:

public static VisualElement<xf.ContentPage> Page(Visuals visuals, Contact initial) => Element.WithContext(ctx => {
    ...    
        return new ContentPage {...};
    });

We must pass to this method a function that takes a single parameter of type LocalContext and returns a blueprint. LocalContext class has a method for working with the local state:

var (state, setter) = ctx.UseLocalState(0);

This method takes an initial state, and returns a tuple with the current state and a setter function that we can use to update the local state.

If you’re familiar with React this is more or less an equivalent of React’s useState hook.

The setter returns a Signal and therefore we can use it in all control events:

...
new Button { Text = $"You clicked {state} times", Clicked = () => setter(state + 1)},
...

The Signal returned by the setter is treated differently by Laconic:

  • It doesn’t trigger evaluation of the main reducer
  • It doesn’t invoke the middleware pipeline
  • All the diffing and patching is applied to the view created with WithContext only
  • The state returned by UseLocalState is only visible to the blueprint making function that received this LocalContext instance

Update 4 Sep 2020: I removed ViewCreated property. A better mechanism is required.

LocalContext has a property ViewCreated that is of type Action<Xamarin.Forms.VisualElement>. This gives us a (limited) access to real Xamarin.Forms views. My Contacts app uses it for setting platform specific properties:

ctx.ViewCreated = p => ((xf.ContentPage)p)
    .On<iOS>()
    .SetModalPresentationStyle(UIModalPresentationStyle.FormSheet);

(But please, use this feature sparringly! Don’t save the reference to the view, and better don’t subscribe to any of its events).

Conclusion

The cleansing of the codebase continues. We got rid of more MVVM boilerplate; made our state, reducer, and UI creation code pure. The code is simpler to write and simpler to understand. And it would be trivial to write unit tests for it have we decided to do so.

Statistics

Original codebase:

-----------------------------------------------------------------
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 Step 3 of the conversion:

-----------------------------------------------------------------
Language       files          blank        comment           code
-----------------------------------------------------------------
C#                49            392            232           2051
MSBuild script     6             11              7            577
XML               12             20             32            476
XAML               6             71             17            424
JSON               4              0              0            157
Markdown           1             23              0             38
Bourne Shell       3              6              2             17
-----------------------------------------------------------------
SUM:              81            523            290           3740
-----------------------------------------------------------------

Character count, XAML and C# files: 115,364


Check out the code for Step 3 of the conversion: https://github.com/shirshov/app-contacts/tree/step-3-ContactEditor-page

Don’t forget to do git submodule init && git submodule update