ImmutableUI and TDD with Xamarin.Forms

MVVM and XAML has failed me. A promise of abstracting to make reusable components, but they never get reused. Layers of complexity for virtually no benefit. If reuse is to happen as planned, it would require almost mystical level foresight into the future, across multiple projects. In the real world, our apps are self contained, and we don’t reuse.

We then code our views in XAML, but what has this given us? A way to neatly separate UI from code? But has it really? When you look at the converters, behaviors and more we need to keep coding, we keep building a bridge between C# and XAML. I have to write a converter, to do what is equivalent of ‘ != ‘ in C#. XAML is verbose and getting out of hand. Then I have to keep wiring up another property to send data back and forth between XAML and the ViewModel.

XAML was also meant to be done by designers, then passed to developers to wire up. I have never in my entire professional career ever seen this happen, and for many reasons, can never see it happening.

ImmutableUI

Spending some time on Flutter, I realized the way they built their UI was incredible. Their UI was an immutable model. You had to rebuild your UI, every time you wanted up to update a property, then send that to the Flutter engine to redraw. In Flutter this works really well, because there is no native components to manage. In Xamarin.Forms, this can’t be done, with the base framework. Frank Krueger built an extension (not production ready just yet) called ImmutableUI, that allows you to work this way. As a basic example, this is how a page will work.

public class MainPage : ContentPage
{
    public MainPage() => Content = BuildView().CreateView();

    void SetState() => BuildView().Apply(Content);

    int _counter;

    Command _push => new Command(() => {
        _counter++;
        SetState();
    });

    ViewModel BuildView() =>
        new StackLayoutModel(
            children: new ViewModel[] {
                new LabelModel(text: _counter.ToString(), fontSize: 42),
                new ButtonModel(text: "Increment", command: _push),
            },
            padding: new Thickness(42));
}

ImmutableUI has a shadow model, where everything has the name Model at the end, e.g. LabelModel, ButtonModel. This is taken, and a differential is done on the actual native components. This means Xamarin.Forms isn’t actually wiping its UI and being rebuilt each time. The UI is built in C#, no XAML here.

Keeping your UI Immutable and in C# has a number of benefits

  1. To change something you have to rebuild it, hence you are aware of the entire UI state every time.
  2. Your UI state is easily accessible in a number of relevant local variables
  3. Since you are in C# you can just write any conversion or presentation code as desired.
  4. C# is a powerful language, you only have to stay in 1 language, and not cross between XAML and C#.

Context

Please read before proceeding.

  1. MVVM makes some more sense in traditional Xamarin, where you actually share the VM’s and have different Views. But I am exclusively talking about Xamarin.Forms in this post.
  2. Yes you can write a good app with MVVM and XAML. My point here is the abstractions are overly tedious and have little value, in most mobile app development scenarios. But that doesn’t mean the end result of an app written in MVVM and XAML is bad.
  3. I won’t be using ImmutableUI in the examples below because:
    1. It’s not production ready, as it doesn’t have events wired up
    2. I wanted to keep things simple. But please note, this is not how you would build the UI in a production app. You would have to use ImmutableUI.
  4. We are using C# here. F# might be able to push further with more immutable functions, but I’m keeping this in the more widely used programming language. Because C# is a an OO language, state and OO concepts are present in these examples.
  5. You can use many different Test frameworks, ReactiveX, IoC containers and so forth. I’m just trying to show the basics. Build upon it how you like, with your favorite frameworks and tools.
  6. I haven’t done this in any real world project. I’m not saying this will solve the worlds development problems, nor that it is completely free of problems.

The idea of this approach is that it leads to a simpler application, and removes patterns that weren’t actually giving us any benefit.

Source Code (GitHub Repo): Pattern

Workflow

First, create an IWorkflow interface and apply to any class. I just tacked it on to the Application class for simplicity. I actually don’t see anything wrong with this approach, but you can pull it into it’s own class if you really want to.

This handles your entire apps navigation.

public interface IWorkflow
{
	Task Navigate(Page page, string state, object parameter);
}

public class App : Application, IWorkflow
{
	public App()
	{
            // You can change this later to whatever your starting page is.
            MainPage = new ContentPage();
	}

	public async Task Navigate(Page page, string state, object parameter)
	{
	}
}

Template Page

The template page is what all new Views will be based off. We are setting this up, so we can easily get into Test Driven Development next. This will closely match the ImmutableUI layout shown above.

public abstract class TemplatePage: ContentPage
{
	protected readonly IWorkflow _navigation;
	public TemplatePage(IWorkflow navigation)
	{
		_navigation = navigation;
		Build();
	}

	protected virtual void Build()
	{
		Content = BuildView();
	}

	protected abstract View BuildView();
}

Testing

It’s testing time and we haven’t even started to write our application yet. Before you get started, we do need some helper code. MockPlatformService.cs and VisualTreeHelper.cs. These just help initialize Xamarin.Forms, and help find elements in your UI. I am using xUnit and Moq for testing, but you can use any testing framework you want.

LoginPage

Picture a simple login page, with 2 entry elements and a button. We know that we need to validate the entry fields, press the button, then navigate to another page. In the main project create an empty LoginPage.

public class LoginPage : TemplatePage
{
	public LoginPage(IWorkflow navigation) : base(navigation) { }
}

In the test project, lets create a new LoginPageTests. This will initialize Xamarin.Forms, create a new IWorkflow mock.

public class LoginPageTests
{
	readonly Mock _workflowMock;
	readonly LoginPage _loginPage;
	public LoginPageTests()
	{
		XamarinFormsMock.Init();
		_workflowMock = new Mock();
		_loginPage = new LoginPage(_workflowMock.Object);
	}
}

What elements will we have on our page? Lets just write them down here. These are just the names of the elements you will have on your page.

readonly string UsernameEntry = "UsernameEntry";
readonly string PasswordEntry = "PasswordEntry";
readonly string LoginButton = "LoginButton";

Now lets create a test. I will just make a simple test to say that the

[Theory]
[InlineData("12345678", "Password", true)]
[InlineData("123456", "Password", false)]
public void Validate_Username(string username, string password, bool isValid)

Inside this test, we confirm the initial state of the UI, make changes, then confirm the button is now enabled.

var view = _loginPage.Content;

var usernameEntry = view.GetElementByName<Entry>(UsernameEntry);
var passwordEntry = view.GetElementByName<Entry>(PasswordEntry);
var buttonEntry = view.GetElementByName<Button>(LoginButton);

// Initial Checks
Assert.False(buttonEntry.IsEnabled);
Assert.True(passwordEntry.IsPassword);
Assert.Null(usernameEntry.Text);
Assert.Null(passwordEntry.Text);

// Act
usernameEntry.Text = username;
passwordEntry.Text = password;

// Get new UI (this is only applicable because I'm not using ImmutableUI)
// Otherwise these 2 lines of code wouldn't be needed
view = _loginPage.Content;
buttonEntry = view.GetElementByName<Button>(LoginButton);

// Assert
Assert.Equal(isValid, buttonEntry.IsEnabled);

This is testing that the button will be enabled when the 2 entry elements meet requirements. This test will fail when run, mainly because the elements aren’t found, but we wrote the tests before the UI. When the button is clicked, we will also want to test that it raised a navigation request.

I flipped navigation on its head. The View doesn’t care about navigation, it has now outsourced that responsibility to the IWorkflow service. Here the LoginPage is expected to say Authenticated, when it is ready to move to the next page. It doesn’t know where it is going, just that its state has changed.

[Fact]
public void Validate_Navigation()
{
	var view = _loginPage.Content;

	// Valid Page
	var usernameEntry = view.GetElementByName(UsernameEntry);
	var passwordEntry = view.GetElementByName(PasswordEntry);
	usernameEntry.Text = "12345678";
	passwordEntry.Text = "12345678";

	var buttonEntry = view.GetElementByName<Button>(LoginButton);
	buttonEntry.Command.Execute(null);

	_workflowMock.Verify(x => x.Navigate(_loginPage, "Authenticated", null));
}

Navigation

Let’s test what should happen when that navigation request is sent. I will create a new AppTests.cs in my test project, initialize and test that the page is moved. Since this is testing that the LoginPage will go to the MainPage, you should create an empty MainPage.cs in your main project first.

public class AppTests
{
	readonly App _app;
	readonly LoginPage _loginPage;

	public AppTests()
	{
		XamarinFormsMock.Init();
		_app = new App();
		_loginPage = new LoginPage(_app);
	}

	[Fact]
	public async void LoginPage_Navigation()
	{
		await _app.Navigate(_loginPage, "Authenticated", null);
		Assert.IsType<MainPage>(_app.MainPage);
	}
}

This test, calls the page type and state, as we just tested the LoginPage does. Now we test the MainPage is of the correct type. This is a simple example, and your real world tests would expand to checking how many pages in the NavigationPage, and so forth. Although you can see how simple that would be to do. Your app navigation is just one small piece of testable logic.

Building the UI

Now that the tests are built, lets build the UI. I added a small extension called TextUpdated, that processes a value when the Text changes. This will not be required once ImmutableUI, has events wired up.

We first build our View. Remember I have shown this using Xamarin.Forms elements, not ImmutableUI, to keep the examples relatable and because ImmutableUI isn’t completed yet. You would never use this in a real world production app.

protected override View BuildView()
{
	return new StackLayout()
	{
		Children = {
			new Entry() { AutomationId="UsernameEntry", Text=_username }.TextUpdated(UpdateEntry),
			new Entry() { AutomationId="PasswordEntry", IsPassword = true, Text=_password }.TextUpdated(PasswordEntry),
			new Button() { Text = "Login", Command=LoginCommand, AutomationId="LoginButton", IsEnabled = _isValid }
		}
	};
}

You will see that building a UI in C# isn’t hard or unreadable. You must use AutomationId to name each element. I have used my workaround, TextUpdated to process TextChanged events. Each time the text changes the UI will be rebuilt.

Note: Attaching to these events is perfect for ReactiveX, and makes more sense in this setup. Rather than hooking into the binding process of a View and ViewModel.

void UpdateEntry(string value)
{
	_username = value;
	Build();
}
void PasswordEntry(string value)
{
	_password = value;
	Build();
}

// State
string _username = null;
string _password = null;
bool _isValid = false;

An immutable UI, means that you update the variables locally, then rebuild the UI again based on the new values.

We can add some validation logic in the build process, to ensure that our Unit Tests pass.

protected override void Build()
{
	Validate();
	base.Build();
}

void Validate()
{
	_isValid = _username?.Length >= 8
		   && _password?.Length >= 8;
}

And finally, we want to wire up the LoginCommand. To keep it simple, I haven’t connected it to the API, but it is simple to do.

Command LoginCommand => new Command(async () =>
{
	Build();

	if (_isValid)
		await _navigation.Navigate(this, "Authenticated", null);
});

I realize this contains a magic string. This is just to ensure the example is simple. You can store these values in constants or enums, or any method you prefer.

Run your Unit tests now, and the LoginPageTests.cs will succeed. Always update the tests with requirements first, then build out your View.

Building the Navigation

This is a simplistic view of a workflow controller. All you need to do, is see which page made the navigation request, see which state they set themselves in, and push the new page. In App.cs in your main project, expand the navigation function as appropriate.

public async Task Navigate(Page page, string state, object parameter)
{
	if (page is LoginPage && state == "Authenticated")
		MainPage = new MainPage(this);
}

Composable Services

What about the rest of the app. Answer is, make everything else a service.

Make an IAPIService, IMyAppService, or ICameraService. Hardware, business logic or repository, there is no reason why a View can’t have a service injected into it directly. ViewModels and Models were just layers that didn’t add much other than more layers, for non-reusable abstraction. Need a place to store state to share across views? Make another service. How big or small you make them, is up to your application requirements. If you were using F# you could do away with many interfaces and have composable functions.

Summary

That’s its. We have Views, we have Services, and we stick them all together as needed. Unit tests are easy to write first, and can even remove the needed for UITest in some scenarios.

I have cut away at many levels of abstraction. You may look at this and think if the interface changes, there will be many places you will have to change. But changes at such a big level in the project, rarely occur, and if they do, you have bigger problems than some interface definition changes. It normally requires entire sections of code to change with it regardless.

This is just what I have come up with and learnt after almost 4 years of Xamarin.Forms development. It may work for you, I do realize it might not work for some situations, but for me, it would have worked better for every single app I came in contact with.

At the end of this post I realized, this switching approach, to a View-First approach. MVVM keeps you thinking about how to display data to your user. It should be the other way around. Your user doesn’t care about your backend, you develop the Views your user wants, then you work out what needs to happen when interacting to services.


Posted

in

by

Tags: