Omnitalented

Converting My Contacts: Step 2. ContactList Page, CollectionView, 3rdParty Controls

July 17, 2020

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

Step 2: ContactList Page

In this step we convert the main page of the app, the contact list:


Unlike with SettingsPage in Step 1, where we set just the Content property of the page, with the Contacts list we create the complete view hierarchy of the ContentPage with Laconic. The content of ContactList.cs is a collection of pure functions and does not create or manipulate any Xamarin.Forms views. The page’s constituent parts: search bar, list, floating action button are created in static methods and are assembled in ContactList.Page static method.

Since the entire structure of the page is created in pure function in a top-down manner, the result is 100% predictable. There’s no sideloading of changes through INotifyPropertyChanged or INotifyCollectionChanged; given the same input, the result is always going to be the same. If what you see on the screen is not what you expect then the investigation should start from the beginning of that method and finish and its end. (Well, custom renderers, of course, can ruin this beautiful theory!)

CollectionView

The original app uses ListView, our updated code uses CollectionView

A list of items displayed in a CollectionView can be quite long, and creating a view for every item would be very inefficient. As you no doubt know both iOS and Android provide mechanisms for recycling views that were scrolled out of the viewport. Xamarin.Forms employs those mechanisms through the way of DataTemplate, DataTemplateSelector, and data binding.

But Laconic, because of its immutable, declarative nature doesn’t use data binding. Blueprints for all items in the source list are created on each rendering request. And Laconic doesn’t expose DataTemplate or DataTemplateSelector. Instead, when adding items to Items list of CollectionView we must provide a reuse key:

static CollectionView List(IEnumerable<Contact> contacts, Visuals visuals)
{
    var colView = new CollectionView();
    foreach (var contact in contacts)
    {
        colView.Items.Add("contactCard", contact.Id, ContactCard(contact, visuals));
    }
    return colView;
}

"contactCard" is our reuse key. It tells the underlying diff/patch mechanism that all views that share this key have the same view hierarchy and can be safely reused. In the case of this app, all items in the list have the same structure and therefore have the same reuse key. For an example of a heterogeneous list see this class in the Demo app.

To make the population of Items more expression-like Laconic provides a helper method which we can use to rewrite the above code:

static CollectionView List(IEnumerable<Contact> contacts, Visuals visuals) => new CollectionView
{
    Items = contacts.ToItemsList(_ => "contactCard", c => c.Id, c => ContactCard(c, visuals))
};

More Middleware

As always, we are trying to make our code pure, as much as possible. And as before we move all the impure parts to middleware. For Step 2 we consolidated all middleware inside App.xaml.cs class. There are three blocks of middleware: one is for communicating visual theme changes with the rest of the app (we added this in Step 1), another one is for handling navigation, and the third is for retrieving data over the network or from the local file system.

The work of converting My Contacts app to using Laconic is an exercise in anger management moving an existing app to MVU architecture piece by piece, view by view, without a total rewrite of everything at once.

While Step 1 dealt only with changing the visual theme of the app, in Step 2 we are entering the business logic territory. To avoid major disruption to the migration process we are reusing the existing services and models, as much as possible. The new code uses the same IDataSource services and made no modifications to Contact model. Since ContactListViewModel is gone, we moved Contacts list to our State, while still keeping the state immutable.

Sharing State

The contact list controls the navigation: from it the user can navigate to contact details or add a new contact. For opening the details page we must pass it the contact that was tapped in the list. The natural way of doing this with Laconic is to send a signal from the list using the contact instance as payload:

static Frame ContactCard(Contact contact, Visuals visuals) => new StyledFrame(visuals.Colors) {
        ...
        GestureRecognizers = {
            ["tap"] = new TapGestureRecognizer {
                Tapped = () => new Signal("showDetails", contact)
            }, 
        }
        ...
}

… and then handle it inside middleware block in App.xaml.cs:

Action action = context.Signal switch {
    ("showDetails", Contact c) => () => navPage.Navigation.PushAsync(new DetailPage(c))
    ...
}
action();

Simple, really, no need to use DependencyService or a framework like Prism!

3rd Party Controls

This app uses PancakeView, a third party control. To bring it into the Laconic’s diff/patch pipeline we must write a binding, which is straightforward:

class PancakeView : Layout<xf.PancakeView.PancakeView>, IContentHost
{
    public View Content { get; set; }

    public xf.CornerRadius CornerRadius
    {
        set => SetValue(xf.PancakeView.PancakeView.CornerRadiusProperty, value);
    }

    public xf.Color BorderColor
    {
        set => SetValue(xf.PancakeView.PancakeView.BorderColorProperty, value);
    }

    public double BorderThickness
    {
        set => SetValue(xf.PancakeView.PancakeView.BorderThicknessProperty, value);
    }
}

PancakeView has more properties, but we only care about those that are being used in this app.

Notice something unusual about this class? The properties are set only! Since Laconic is declarative we don’t need to provide getters, everything works fine without them.

Hmm… Mixed Signals

The code of Step 2 shows two different notations of writing switch exressions for processing signals:

context.Signal switch {
    ("showDetails", Contact c) => () => navPage.Navigation.PushAsync(new DetailPage(c)),
    ...
}

vs.

context.Signal switch {
    DataReceived rec => new State(false, rec.Payload.ToArray(), state.Visuals),
    ...
}

The difference is just a matter of style, both ways are equivalent from the library’s point of view. The former is quicker to write, the latter requires a bit of boilerplate but provides type safety:

class DataReceived : Signal<IEnumerable<Contact>>
{
    public DataReceived(IEnumerable<Contact> payload) : base(payload)
    {
    }
}

The constructor of the stock Signal class takes two arguments, the first is typically used as a signal identifier and the optional second one as payload. It also provides a Deconstruct method that can be used in switch expressions as shown in the first example above.

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

-----------------------------------------------------------------
Language       files          blank        comment           code
-----------------------------------------------------------------
C#                51            439            251           2259
XAML               7             90             17            664
MSBuild script     6             11              7            573
XML               12             20             32            476
JSON               4              0              0            157
Markdown           1             23              0             38
Bourne Shell       3              6              2             17
-----------------------------------------------------------------
SUM:              84            589            309           4184
-----------------------------------------------------------------

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

Well, our C# numbers keep growing… But hey, another XAML file, another ViewModel is gone! Good stuff. Also, compared to Step 1 (which necessarily laid some infrastructure) we removed 1350 characters! Some progress.

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

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