[EN] What bothers Xamarin developers? Part 3

It’s time for a third part of articles series about what bothers Xamarin developers. The goal of this series is to identify things that should be improved. We all love Xamarin and we want to make it better by talking about things that bother us.

It’s time for a third part of article series about what bothers Xamarin developers. The goal of this series is to identify things that should be improved. We all love Xamarin and we want to make it better by talking about things that bother us.

So far there are these post in the series:

Today I would like to introduce you to Maciej Sznurowski, who has many interesting things to share with you. Let’s check his opinions.


Maciej Sznurowski. .NET Developer with over 5 years of commercial experience in various Microsoft technologies such as: WinForms, WPF, Windows Services, ASP.NET Web Forms, ASP.NET MVC, ASP.NET Core MVC, UWP. Passionate heavy metal drummer and author of growing metronome app called Camtronome for Android and iOS. Camtronome is written in Xamarin.Forms with a lot of Xamarin.Android and Xamarin.iOS components. You can contact Maciej via email.

Xamarin developer difficulties

I have been working on Camtronome metronome in Xamarin for over 2 years now. Development process has many advantages like small learning curve for .NET developer, but on the other hand development can be extremely frustrating and discouraging or even impossible. In this post I’d like to share obstacles and difficulties I have experienced through all my Camtronome development time. I will also include workarounds I found out and provide my wishes regarding Xamarin future. Please note that observations below are my personal point of view about development during over 2 years with Xamarin.

Mac Agent connection drops, missed breakpoints and build inconsistency

Story below is a very short summary of my experience with Xamarin.iOS on Windows.

I work on Windows 10 machine connected to Mac Book Air by Xamarin Mac Agent. Usually, first connection, build and deploy works pretty well. I launch Visual Studio, connect to Mac Agent and hit F5 to debug. Build is mostly done on Mac, but some MSBuilds on Windows are also present. 3 minutes and Camtronome is launched on iPhone, great. I start coding, work hard, it takes time and after about 10 minutes I hit F5 to check if I my code works as expected. Build process begins, I see its outputs in Visual Studio, build succeeds, I expect iPhone to display Camtronome and debugger to stop at breakpoint, but… Pair to Mac window appears:When I first worked with iOS on Xamarin and saw this, I couldn’t really figure out what has just happened – build succeed, Mac Agent built its part and connection suddenly broke. Too bad, manual reconnection is really required.

I’m back in game – I have reconnected. I hit F5 one more time, Camtronome deploys, launches, but my breakpoint isn’t hit, even though Visual Studio claims all symbols have been loaded. Too bad, I need that breakpoint to be hit, I know its code has executed, because I see changes in my app. When I first encountered this, multiple SO comments suggested to perform full clean and re-deploy the app.

Ok, let’s do that then. I clean solution, wait approx 1 minute for solution to clean, hit F5 to deploy the app again – rebuild process starts, takes 3 more minutes and…

An inconsistency between the local app and the remote build has been detected for Camtronome. Please rebuild the application and try again. Check the log for more details

I search the forums again and what do I find? Clean solution and re-deploy the app…

This is real, this is how it works. At this point I usually clean again, relaunch Visual Studio and redeploy one more time and then it usually works, but sometimes this problem persists until even one more Visual Studio relaunch. All these steps take time, at some point I’m so focused on making my metronome app run again that I literally forget what I was working on. This is an extremely discouraging and frustrating experience and I would really be pleased if things of this area went better and more stable in future.

Application startup time

In this chapter, I will show the problem of slow startup time and my approach to mitigate it. All examples below are launched on Android phones (Samsung Galaxy S4 mini and Samsung Galaxy S8), but this problem and its mitigation applies to iOS as well.

Let’s begin with simple “Hello world” application with one ContentPage, which has single label:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="LaunchPerformanceTest.MainPage">
    <ContentPage.Content>
        <StackLayout>
            <Label Text="Welcome to Xamarin.Forms!" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Even such a simple application launches a while on non-flagship phones. We can measure it by modifying the MainActivity code:

public class MainActivity : FormsAppCompatActivity
{
    private long start;
    private long end;

    protected override void OnCreate(Bundle bundle)
    {
        start = Java.Lang.JavaSystem.CurrentTimeMillis();

        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;

        base.OnCreate(bundle);

        global::Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new App());
    }

    protected override void OnResume()
    {
        base.OnResume();

        end = Java.Lang.JavaSystem.CurrentTimeMillis();
        System.Diagnostics.Debug.WriteLine($"LAUNCHING TIME: {end - start}");
    }
}

Of course, time amount measured doesn’t actually show the real startup time, but can give us a bright insight on how long our application launches. The bigger the value, the longer the time our users see the splash screen (or empty uninitialized activity).Simple “Hello world” startup time measurements are below:

Both values are the best what we can achieve in Xamarin.Forms, which, unfortunately, is nothing we can do with as this is because of Forms framework initialization. From this point each new view, binding, NuGet package, project reference, etc, increases the time required for application to launch.

Of course, no application has single label on its start page. We have View Models, database calls, some classes to be initialized, data bindings to be performed. All that things impact the startup time. Let’s simulate some real world scenarios by making the MainPage.xaml a bit more complex. Please note that page defined below is very unoptimised, but this is just to demonstrate the problem and my approach to mitigate it as much as possible.

<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LaunchPerformanceTest.MainPage">

    <ScrollView>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>

            <Grid Grid.Row="0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <StackLayout Grid.Column="0">
                    <Label Text="Test text" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" />
                    <Button Text="Test button" />

                    <StackLayout HorizontalOptions="Center" VerticalOptions="CenterAndExpand">
                        <Image Source="image.png" Aspect="AspectFit" />
                    </StackLayout>

                    <StackLayout>
                        <Entry Text="Entry test" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
                        <Entry Text="Entry test 2" VerticalOptions="CenterAndExpand"
                               HorizontalOptions="CenterAndExpand" />
                    </StackLayout>
                </StackLayout>

                <StackLayout Grid.Column="1" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
                    <Image Source="image.png" Aspect="AspectFit" />
                    <Entry Text="Entry test 3" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />

                    <StackLayout Orientation="Horizontal" HorizontalOptions="StartAndExpand">
                        <Label Text="First switch test" />
                        <Switch />
                    </StackLayout>
                </StackLayout>
            </Grid>

            <Grid Grid.Row="1">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <StackLayout Grid.Column="0" HorizontalOptions="StartAndExpand">
                    <Label Text="Welcome to Xamarin.Forms!" HorizontalOptions="Center"
                           VerticalOptions="CenterAndExpand" />

                    <StackLayout Orientation="Horizontal" HorizontalOptions="StartAndExpand">
                        <Label Text="Switch test" />
                        <Switch />
                    </StackLayout>
                </StackLayout>

                <StackLayout Grid.Column="1" HorizontalOptions="StartAndExpand">
                    <Button Text="Button before entry" />
                    <Entry Text="Entry under button" />
                    <Button Text="Button below entry" VerticalOptions="CenterAndExpand"
                            HorizontalOptions="CenterAndExpand" />
                </StackLayout>

                <StackLayout Grid.Column="2" HorizontalOptions="StartAndExpand">
                    <Button Text="Complex button" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
                    <Button Image="image.png" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />

                    <StackLayout Orientation="Horizontal" HorizontalOptions="StartAndExpand">
                        <Label Text="More switch" />
                        <Switch />
                    </StackLayout>
                </StackLayout>
            </Grid>

            <StackLayout Grid.Row="2">
                <Button Text="Button 1" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
                <Button Text="Button 2" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
                <Button Text="Button 3" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
                <Button Text="Button 4" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
                <Image Source="image.png" Aspect="AspectFit" />
            </StackLayout>
        </Grid>
    </ScrollView>
</ContentPage>

Having such startup page makes our application launch over 50% longer:

During my Camtronome metronome development I had a sad record of over 16 000 ms of that value on Galaxy S4 mini. I had to do something with it. After many days of optimising my XAML views I lowered that value to about 14 000 ms, which was still unacceptable for me. I want my metronome to launch as fast as possible to avoid losing users. One day I woke up with an uncommon approach idea I called the async view initialization.

Let’s look at our unoptimized MainPage.xaml – we have a ScrollView containing 3-rows Grid here. Each Grid row has its own heavy layout inside. For the purpose of this experiment – let’s refactor that Grid and move all 3 rows to 3 different ContentViews:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:LaunchPerformanceTest" x:Class="LaunchPerformanceTest.MainPage">

    <ScrollView>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>

            <local:Row1 Grid.Row="0" />

            <local:Row2 Grid.Row="1" />

            <local:Row3 Grid.Row="2" />
        </Grid>
    </ScrollView>
</ContentPage>

Of course, this operation didn’t change our startup times in any way, but opened a simple way to implement the async view loading idea.

Now, let’s remove initialization of our ContentViews from Grid, leaving it just with its Row Definitions:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:LaunchPerformanceTest" x:Class="LaunchPerformanceTest.MainPage">

    <ScrollView>
        <Grid x:Name="GridRoot">
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>

        </Grid>
    </ScrollView>
</ContentPage>

We can now proceed with async view loading. The idea is to initialize as many views as possible in code-behind in separate, multiple threads. As soon as views are initialized, add it to the layout using UI thread. Such idea makes our unoptimized MainPage defined both in xaml and in code behind in C#. Xaml contains only the layout definition and code behind – actual Content Views initialization:

public partial class MainPage : ContentPage
{
    private Row1 _row1;
    private Row2 _row2;
    private Row3 _row3;

    public MainPage()
    {
        InitializeComponent();
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        CreateViews();
    }

    private void CreateViews()
    {
        Task.Run(() => CreateRow1());
        Task.Run(() => CreateRow2());
        Task.Run(() => CreateRow3());
    }

    private void CreateRow1()
    {
        if (_row1 == null)
        {
            _row1 = new Row1();

            Device.BeginInvokeOnMainThread(() =>
            {
                GridRoot.Children.Add(_row1);
                Grid.SetRow(_row1, 0);
            });
        }
    }

    private void CreateRow2()
    {
        if (_row2 == null)
        {
            _row2 = new Row2();

            Device.BeginInvokeOnMainThread(() =>
            {
                GridRoot.Children.Add(_row2);
                Grid.SetRow(_row2, 1);
            });
        }
    }

    private void CreateRow3()
    {
        if (_row3 == null)
        {
            _row3 = new Row3();

            Device.BeginInvokeOnMainThread(() =>
            {
                GridRoot.Children.Add(_row3);
                Grid.SetRow(_row3, 2);
            });
        }
    }
}

Having our MainPage defined this way, we can be satisfied again – our application has now the perceived startup time even slightly better that “Hello world” initially had:

However, this is a tricky measurement, because we actually didn’t reduce the real application startup time. What we did instead is we reduced the time our users would see the splash screen (or empty main activity) before the full page is loaded and initialized. In this scenario the Main Page appears “immediately” and its children appears one by one. You can even notice that all 3 grid rows appears slightly one by one, even on flagship phones. I find this a way better user experience compared to extended time of splash screen being displayed. A cool thing is that this approach can be applied to other pages our application navigates to, which also reduces perceived time spent for page navigation.

As usual, this approach has its disadvantages – it makes the code extremely messy and difficult to maintain. Imagine that you had data bindings, commands binding, different Binding Contexts to be assigned – all that operations would be declared in CreateRow1, CreateRow2 and CreateRow3 methods before calling UI thread to add our views to view hierarchy. Most of Camtronome views are declared that way and – believe me – changing anything there is a tough challenge. However, I’m convinced that the payoff is big enough not to complain about code maintainability. I think this approach would be a great feature if Xamarin.Forms framework did this by default, or at least opened a way to enable it.

Summary

Xamarin is a great framework, but it has its drawbacks too. Apps development using Xamarin can sometimes be quite painful and problematic – long build and deploy times and troubles with iOS development on Windows are one of many obstacles developers may encounter. Applications performance could have also been better – especially the Forms startup times.

However, I’m convinced that literally any problem (even a big one) with Xamarin can be solved in a small amount of time compared to native programming in Java / Kotlin or Objective-C / Swift. I would have never been able to create in 2 years (besides full-time .NET developer job) such a feature-rich metronome app as Camtronome solely. Not mentioning the learning curve of native frameworks, but also the amount of code required to create a native app. I have some basic experience with native Android and I’m pretty sure that native Camtronome on Android would have more code to be written than it is already written in Xamarin. I hope this framework will further improve, making it even better.


What do you think about Maciej opinions? Do you agree? Let me know in the comments!

This article series will take a break for some time. Stay tuned for another post in the future.

  1. And these reasons are why i switched to flutter. I started with Xamarin early with Version 1.1. Nearly every update breaks something. Xamarin lost me as a developer.

    Odpowiedz

Dodaj komentarz

%d bloggers like this: