TaskLoaderView & Lottie Best Friends Forever

TaskLoaderView & Lottie Best Friends Forever

You should all know Lottie by now!
If you don't let me just tell you that it's an amazing way to give an identity to your app.

Lottie is a cross platform library created by Twitter, then wrapped in a cool Xamarin.Forms view by Martijn van Dijk. Thanks to Lottie, you can import as json the coolest animations created by your favorite designer.

For this, your best friend designer needs to create the animation in Adobe After Effects, and export it with the Bodymovin plugin.

Now if you don't have a designer "under the elbow" (popular french saying), you can browse the Lottie Files website and find amazing animations.

I chose the "Nostalgia Pack" by Elemental Concept, cause it matched the retro spirit of my retronado app.

You can even edit the colors of the lottie files thanks to the Lottie Editor.

This is what I did here and changed the animation color to match my primary and background color.

The identity of your app

App identity should be a major phase in your app creation process.
My favorite kind of branding is the subtle one, the minimal one.
With one color and very few cool animations or icons (maybe a font), you could achieve great identity.
This is material design philosophy. Modern apps tend to follow that principle: icons are no longer shown in the application toolbar for example.

So where could we put our subtle animations?

  • Loading screen!
  • Error Screen!
  • Empty Screen!

Fortunately, those views are all handled by the TaskLoaderView making it super easy to implement.

At the same time Lottie really shines at providing simple animations.
Your designer can create several animations with the Bodymovin plugin and you can embed them so easily in your Xamarin.Forms app. All you have to do is add the json files in your Assets folder in Android, and Resources folder in iOS.

remark: you can notice I didn't mention the Splash screen. If you can, it should be avoided to have a smoother ux.

Android Assets
Android Assets Folder

iOS Resources
iOS Resources Folder

So let's start the fusion between the TaskLoaderView and Lottie!

TaskLoaderView and Lottie views

The TaskLoaderView is a loading state container.
It provides you with views for each state of your ViewModel:

  • Loading
  • Error
  • ErrorOnRefresh
  • Empty
  • NotStarted

If you use the TaskLoaderView in your views and the TaskLoaderNotifier in your view models, you can say goodbye to all the properties like IsBusy, HasErrors, IsRefreshing, ErrorMessage, ... Everything is handled gracefully thanks to composition.

You can find the github repo and package here: https://github.com/roubachof/Sharpnado.TaskLoaderView

The blog post about version 2.0 is here: https://www.sharpnado.com/taskloaderview-2-0-lets-burn-isbusy-true/

Now since version 2.0 of the TaskLoaderView you can provide your own custom views for the loading states. We will use them to display the Lottie animations and level-up our retronado identity \o/

Lottie retronado

Now let's have a look at our Retronado sample app and our Lottie page:

LottieViewsPage.xaml

<customViews:TaskLoaderView Grid.Row="2"
                            Style="{StaticResource TaskLoaderStyle}"
                            TaskLoaderNotifier="{Binding Loader}">
    <customViews:TaskLoaderView.LoadingView>
        <lottie:AnimationView x:Name="LoadingLottie"
                              AbsoluteLayout.LayoutFlags="PositionProportional"
                              AbsoluteLayout.LayoutBounds="0.5, 0.4, 120, 120"
                              HorizontalOptions="FillAndExpand"
                              VerticalOptions="FillAndExpand"
                              Animation="{Binding Loader.ShowLoader, Converter={StaticResource CyclicLoadingLottieConverter}}"
                              AutoPlay="True"
                              Loop="True" />
    </customViews:TaskLoaderView.LoadingView>

    <customViews:TaskLoaderView.EmptyView>
        <StackLayout AbsoluteLayout.LayoutFlags="PositionProportional" 
                     AbsoluteLayout.LayoutBounds="0.5, 0.4, 300, 180">

            <lottie:AnimationView HorizontalOptions="FillAndExpand"
                                  VerticalOptions="FillAndExpand"
                                  Animation="empty_state.json"
                                  AutoPlay="True"
                                  Loop="True" />

            <Label Style="{StaticResource TextBody}"
                   HorizontalOptions="Center"
                   VerticalOptions="Center"
                   Text="{loc:Translate Empty_Screen}"
                   TextColor="White" />
            <Button Style="{StaticResource TextBody}"
                    HeightRequest="40"
                    Margin="0,20,0,0"
                    Padding="25,0"
                    HorizontalOptions="Center"
                    BackgroundColor="{StaticResource TopElementBackground}"
                    Command="{Binding Loader.ReloadCommand}"
                    Text="{loc:Translate ErrorButton_Retry}"
                    TextColor="White" />
        </StackLayout>
    </customViews:TaskLoaderView.EmptyView>

    <customViews:TaskLoaderView.ErrorView>
        <StackLayout AbsoluteLayout.LayoutFlags="PositionProportional" 
                     AbsoluteLayout.LayoutBounds="0.5, 0.4, 300, 180">

            <lottie:AnimationView HorizontalOptions="FillAndExpand"
                                  VerticalOptions="FillAndExpand"
                                  Animation="{Binding Loader.Error, Converter={StaticResource ExceptionToLottieConverter}}"
                                  AutoPlay="True"
                                  Loop="True" />

            <Label Style="{StaticResource TextBody}"
                   HorizontalOptions="Center"
                   VerticalOptions="Center"
                   Text="{Binding Loader.Error, Converter={StaticResource ExceptionToErrorMessageConverter}}"
                   TextColor="White" />
            <Button Style="{StaticResource TextBody}"
                    HeightRequest="40"
                    Margin="0,20,0,0"
                    Padding="25,0"
                    HorizontalOptions="Center"
                    BackgroundColor="{StaticResource TopElementBackground}"
                    Command="{Binding Loader.ReloadCommand}"
                    Text="{loc:Translate ErrorButton_Retry}"
                    TextColor="White" />
        </StackLayout>
    </customViews:TaskLoaderView.ErrorView>

    ...

</customViews:TaskLoaderView>

Thanks to the TaskLoaderView AbsoluteLayout, we can position the views very easily.

To have a better experience, we're selecting the animation according to the Exception type:

namespace Sample.Converters
{
    public class ExceptionToLottieConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
            {
                return null;
            }

            string imageName;

            switch (value)
            {
                case ServerException serverException:
                    imageName = "server_error.json";
                    break;
                case NetworkException networkException:
                    imageName = "connection_grey.json";
                    break;
                default:
                    imageName = "sketch_grey.json";
                    break;
            }

            return imageName;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // One-Way converter only
            throw new NotImplementedException();
        }
    }
}

And for the loading animation we're cycling between the 3 different ones:

namespace Sample.Converters
{
    public class CyclicLoadingLottieConverter : IValueConverter
    {
        private int _counter = -1;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null || value is bool showLoader && !showLoader)
            {
                return null;
            }

            _counter = ++_counter > 2 ? 0 : _counter;
            switch (_counter)
            {
                case 0:
                    return "delorean_grey.json";
                case 1:
                    return "joystick_grey.json";
                case 2:
                    return "cassette_grey.json";
            }

            return "delorean_grey.json";
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // One-Way converter only
            throw new NotImplementedException();
        }
    }
}

For the view model part, it's exactly the same than the others cases. The TaskLoaderNotifier is doing wonders by handling all the properties and notify change events:

RetroGamesViewModel.cs

namespace Sample.ViewModels
{
    public class RetroGamesViewModel : ANavigableViewModel
    {
        private readonly IRetroGamingService _retroGamingService;

        private readonly ErrorEmulator _errorEmulator;

        private GamePlatform _platform;

        public RetroGamesViewModel(
            INavigationService navigationService, 
            IRetroGamingService retroGamingService, 
            ErrorEmulator errorEmulator)
            : base(navigationService)
        {
            _retroGamingService = retroGamingService;
            _errorEmulator = errorEmulator;

            ErrorEmulatorViewModel = 
                new ErrorEmulatorViewModel(errorEmulator,  () => Loader.Load(InitializeAsync));

            Loader = new TaskLoaderNotifier<List<Game>>();
        }

        public TaskLoaderNotifier<List<Game>> Loader { get; }

        public ErrorEmulatorViewModel ErrorEmulatorViewModel { get; }

        public override void Load(object parameter)
        {
            _platform = (GamePlatform)parameter;

            Loader.Load(InitializeAsync);
        }

        private async Task<List<Game>> InitializeAsync()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            var result = _platform == GamePlatform.Computer
                             ? await _retroGamingService.GetAtariAndAmigaGames()
                             : await _retroGamingService.GetNesAndSmsGames();

            watch.Stop();
            var remainingWaitingTime = TimeSpan.FromSeconds(4) - watch.Elapsed;
            if (remainingWaitingTime > TimeSpan.Zero)
            {
                // Sometimes the api is too good x)
                await Task.Delay(remainingWaitingTime);
            }

            switch (_errorEmulator.ErrorType)
            {
                case ErrorType.Unknown:
                    throw new InvalidOperationException();

                case ErrorType.Network:
                    throw new NetworkException();

                case ErrorType.Server:
                    throw new ServerException();

                case ErrorType.NoData:
                    return new List<Game>();

                case ErrorType.ErrorOnRefresh:
                    if (DateTime.Now.Second % 2 == 0)
                    {
                        throw new NetworkException();
                    }

                    throw new ServerException();
            }

            return result;
        }
    }
}