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 usesCollectionView
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 CornerRadius CornerRadius
{
set => SetValue(xf.PancakeView.PancakeView.CornerRadiusProperty, value);
}
public 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