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 thisLocalContext
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