Omnitalented

Introducing Laconic

May 12, 2020

Laconic is a library for creating mobile apps with Xamarin Forms using React + Redux like approach.

MVVM out, MVU in

It’s a safe bet to make that most Xamarin.Forms applications are written using Model-View-ViewModel (MVVM) architecture. It’s a tried and true approach that served us well.

But in recent years another style of making UIs for web and mobile apps is gaining in popularity: Model-View-Update (MVU). It originated with Elm project and therefore often called Elm Architecture. Perhaps the most visible example of a framework using MVU is React / React Native, and other popular ones are Vue.js, Flutter, and SwiftUI.

Xamarin Forms community has its own MVU library: it’s a fabulous an excellent Fabulous project. Alas, it’s really only usable from F#… And this is where Laconic steps in, with the purpose of making writing MVU with Xamarin.Forms as comfortable as possible, using plain C#.

Anatomy of Laconic app

Let’s see how a simple Counter app is written using Laconic.

counter ui

(M) The first building block is state. A state is a plain C# object which should contain all values that the app displays or manipulates. There are no special requirements for the state. For the Counter app code in this article the state is just an int.

(V) Next, we are going to write a function that takes a state and returns a view:

static StackLayout Counter(int state) => new StackLayout {
    Padding = 50,
    ["lbl"] = new Label {
        Text = $"You clicked {state} times",
        FontSize = 30
    },
    ["btn"] = new Button {
        Text = "Click Me",
        TextColor = xf.Color.White,
        FontSize = 20,
        BackgroundColor = xf.Color.Coral,
        BorderColor = xf.Color.Chocolate,
        BorderWidth = 3,
        CornerRadius = 10
    }
};

Looks very familiar, doesn’t it? Just like hundreds of examples for creating Xamarin.Forms UI in C#. But there’s an important difference: StackLayout, Label and Button classes are not from Xamarin.Forms namespace. They are provided by Laconic. Those classes are virtual representation of what the app intends to show on the screen. The API of those clases mostly mirrors the API of classes in Xamarin.Forms namespace.

(Some types don’t have representation in Laconic, e.g. Xamarin.Forms.Color. Those types are used directly, via xf alias to Xamarin.Forms namespace.)

To make the distinction between views of Xamarin.Forms and virtual views of Laconic more clear, we call virtual views blueprints, and the functions that create them blueprint makers.

(U) When the user taps on the button the app needs to update the label that shows the number of clicks. In Laconic all changes to the state are done by sending signals. To announce that the button was clicked we are going to add the following line:

    ["btn"] = new Button {
        Clicked = () => new Signal("inc"),
        ...
    }

All events raised by Xamarin.Forms controls are represented in their Laconic counterparts as properties that should be set to an expression returning an instance of Signal class. An instance of Signal carries a payload, in this case the payload is a string “inc” that indicates that the state value should be incremented.

The final building block we need to provide is a reducer. Reducer is a function that takes the current state, the signal that was sent, and returns a new state:

static int Reducer(int state, Signal signal) => signal.Payload switch {
   "inc" => state + 1,
   _ => state
};

Now we can tie it all together using Binder, then create a Xamarin.Forms view (StackLayout) and set Content property of the ContentPage that is hosting our Counter UI:

var initialState = 0;
_binder = Binder.Create(initialState, Reducer);
Content = _binder.CreateView(Counter);

How It All Works

The execution flow of an app built with Laconic can be shown like this:

The code in blue boxes is what the app developer writes, the rest is provided by Laconic. The library keeps track of the Xamarin.Forms views it has created, and the blueprints they were created from. When a signal is received Laconic calls the reducer provided by the app. The reducer returns a new state, which is passed to the blueprint makers. Then Laconic calculates what was changed between the stored blueprints and newly returned ones, and updates Xamarin.Forms views.

The Goal: Simplicity

The question you might ask: why go through all the trouble of implementing a completely new approach?

It is my firm belief that this is a better way of making UI. Mobile app code is inherently stateful. Maintaining the state, and updating the screen when the state changes, is not trivial. The task is further complicated by the fact that updates to the state can come from different sources.

With MVVM the app’s state is scattered among many view models, making it hard to see the whole picture. Compare it to Laconic’s approach: there’s one state object, and it’s the ultimate source of truth.

View models often need to communicate with each other. This is usually done with a sort of pubsub mechanism, whether it’s using Observables or events or something like MessagingCenter. This pubsub must be implemented with extreme care, otherwise it quickly goes out of hand, making it hard to trace where a particular update to the VM’s state came from.

With Laconic the flow is super simple: anything that happens in the app or the system is a signal. User tapped on screen? A signal. Network data arrived? A signal. Device orientation changed? A signal.

The state can only be changed by the reducer. When Laconic receives a signal it calls the reducer, and only one reducer can be running at any moment in time. There’s no chance of cross-thread race conditions, no way for any other service or VM to change (and potentially corrupt) the state.

One very important property of this architecture is that the execution flow is completely top-down. “What you read [in code] is what you get”. The reducer is top-down: just by reading it from top to bottom you should get a good idea of how the app reacts to things changing in and around it.

Blueprint functions are top-down too. The result of executing one is a complete and unambiguous representation of how the app screen should look at this moment in time. There is no data binding that can make changes to control properties when you least expect it!

App code becomes a collection of simple, pure functions that are easy to write, easy to understand. The code becomes more modular: composing UI from a collection of small functions is much easier than writing a lot of boilerplate for custom Xamarin.Forms controls.

And just think about how trivial it is to write tests for reducers and blueprint functions: no mocking is required, simply pass values to a function and check that the returned value is what it should be!

Conclusion

By using Elm architecture with virtual view representations Laconic makes writing Xamarin.Forms applications significantly simpler than the mainstream approach of XAML + MVVM.

Check it out

If any of the above sounds interesting then get the source code, check the demo app: https://github.com/shirshov/laconic

Questions, suggestions? Contact me on Twitter: https://twitter.com/omnitalented


P.S.

Q: But what about performance? All of the above sounds like a lot of added overhead.

A: Don’t worry. Performance is adequate even without any optimisation work done. Check out this silly demo: https://github.com/shirshov/laconic/blob/master/demo/app/DancingBars.cs