Enhance Your Xamarin.Forms App’s User Experience With Polly

When working on an app that relies on getting data from an API, it is crucial to make the waiting and loading experience as seamless as possible for the user – like Facebook, Amazon, LinkedIn apps do. Unresponsive app will most likely be uninstalled by the user and you can say goodbye to your customers, revenue, user base, whatever it is you made the app for.

One of the biggest issue API reliant apps face is unreliable network connectivity. As users move around with their mobile devices, they are bound to run into poor or dropped network connectivity issues. We must have a strategy around transient faults and network issues, and that’s where Polly comes to the rescue.

Polly has been around for a while and is a great tool for making your app more responsive. It helps in developing strategies around transient faults i.e. dropped network connection, weak transmission resulting in errors, etc. In this post, I will demonstrate how to simplify the use of Polly in our Xamarin.Forms MVVM app. You can build from there and make the strategies as complex as needed.

Polly works with any .NET application. Which means that you can apply these resilience policies to your .NET API layer as well.

Scenario

Let’s build an app that pulls a list of countries from a REST API. If the API is ever down or not responsive for a period, we still want to get the data without throwing up a bunch of alert messages. To do this, we employ a policy to retry a given number of times if a call fails at first. And in some cases, wait before retrying to give the API time to recover and cater to our requests.

Architecture

polly1

We structure our code in several layers/nodes/tiers however you want to call them. This will keep the code decoupled based on behavior. We will have the following layers,

  • Models – for all our model objects
  • Common – resources and anything else used app wide
  • Services – services to make API calls
  • Core – Shared MVVM code
  • iOS – iOS platform specific code
  • Android – Android specific code

In my opinion, the best OSS framework currently available in the

Prism Library

market to write MVVM Xamarin.Forms app is Prism.Forms. It provides services to handle navigation, dependency injection, events, etc. So we will be using Prism.Forms for this app.

Components

These are the main components of the app that make use of Polly as a stand-alone service that can be used optionally.

polly

IClient

We will use the mono managed HttpClient for this demo. But since we can swap the implementation of the client for numerous reasons, we will keep our client independent using IClient abstraction.

INetworkService

This service will handle our Polly policies. We want to keep all my policies in one place so that we can easily manage them and test them.

IApiService

This service will handle all API calls. We can choose to use Polly policies (via INetworkService) or not in this service. We, as developers, have the freedom to call APIs as we need since not all calls need a policy e.g. POST that can cause duplication or errors will not be retried. All our functional services will inject IApiService to make API calls.

The main component to pay attention to here is the INetworkService. We inject this service in the ApiService and use for selective calls. This allows us to mock and test our services and gives us the option to replace Polly with any other implementation in the future without changing code in all services, just like our IClient… de-coupled 😉.

Resilience Policies

We can have a policy for any type of exception we want to handle. For this app, however, we will implement Retry and WaitAndRetry policies based on the catch all Exception since we do not have any special flow for different exceptions. We use these as a starting point and build on top to get more complex policies for our app. Check Polly’s documentation to learn about all its powers.

Let’s start with the INetworkService with Polly policies.

Retry

Our Retry method will take a Task to execute on retry, a retryCount, and a Func to execute on each retry.

WaitAndRetry

Much like Retry, our WaitAndRetry will add a wait time between each retry giving the API server to recover (and show a status to the user while they wait, log, etc.)

Here is how our completed INetworkService will look like,


public interface INetworkService
{
Task<T> Retry<T>(Func<Task<T>> func, int retryCount, Func<Exception, int, Task> onRetry);
Task<T> WaitAndRetry<T>(Func<Task<T>> func, Func<int, TimeSpan> sleepDurationProvider, int retryCount,
Func<Exception, TimeSpan, Task> onRetryAsync);
}

The implementation for Retry will be as such,


internal async Task<T> RetryInner<T>(Func<Task<T>> func,
int retryCount,
Func<Exception, int, Task> onRetry)
{
return await Policy
.Handle<Exception>()
.RetryAsync(retryCount, onRetry)
.ExecuteAsync<T>(func);
}

and our WaitAndRetry will be implemented like,


internal async Task<T> WaitAndRetryInner<T>(Func<Task<T>> func, Func<int, TimeSpan> sleepDurationProvider,
int retryCount,
Func<Exception, TimeSpan, Task> onRetryAsync)
{
return await Policy
.Handle<Exception>()
.WaitAndRetryAsync(retryCount, sleepDurationProvider, onRetryAsync)
.ExecuteAsync<T>(func);
}

As mentioned before, we are only handling exceptions of type System.Exception, but we can handle other exceptions e.g. .Handle<SqlException> and retry database connection, .Handle<HttpResponseException> and handle response issues, etc. We can also combine these as .Handle<SqlException>().Or<HttpResponseException>().

Usage

Now that we have our policies in place, we will simply inject INetworkService into our IApiService and use its policies.


public class ApiService : IApiService
{
readonly IClient _client;
readonly INetworkService _networkService;
public ApiService(IClient client, INetworkService networkService)
{
_client = client;
_networkService = networkService;
}
// No policies
public async Task<T> GetAsync<T>(Uri uri) where T : class
{
return await ProcessGetRequest<T>(uri);
}
// With policies
public async Task<T> GetAndRetry<T>(Uri uri, int retryCount, Func<Exception, int, Task> onRetry = null) where T : class
{
var func = new Func<Task<T>>(() => ProcessGetRequest<T>(uri));
return await _networkService.Retry<T>(func, retryCount, onRetry, cancelToken);
}
public async Task<T> GetWaitAndTry<T>(Uri uri, Func<int, TimeSpan> sleepDurationProvider, int retryCount, Func<Exception, TimeSpan, Task> onWaitAndRetry = null) where T : class
{
var func = new Func<Task<T>>(() => ProcessGetRequest<T>(uri));
return await _networkService.WaitAndRetry<T>(func, sleepDurationProvider, retryCount, onWaitAndRetry, cancellationToken);
}
}

Finally, we will call the IApiService from our functional service,

public class SomeService
{
    IApiService apiService;
    public SomeService(IApiService apiService)
    {
        _apiService = apiService;
    }
}

Retry,
var response = await _apiService.GetAndRetry(host, DEFAULT_COUNT, OnRetry);

Wait and retry,
var response = await _apiService.GetWaitAndRetry(host, GetSleepDuration, DEFAULT_COUNT, OnWaitAndRetry);

That’s it. Now we can customize the behavior in OnRetry and/or OnWaitAndRetry and control the UI flow.

Making Sense Of It All…

So, what did we just do. We introduced a new service that holds all our resilience policies. Why keep it in a service in itself? For one, it gives us the ability to test our services with and/or without resilience policies. It let’s us swap Polly with any other implementation when the time comes by simply swapping out one service. Finally, it let’s us introduce resilience policies later in the project with minimum effort.

Fancy Bonus

How about we fancy our app a bit and let the user know that we are working hard on their request. What I would like to do is to change my Loading... text to Still loading... on my 2nd retry. To do so, we may want to bring our retry delegates into the ViewModel to have better control. But in this app, the service will simply publish an event that it is retrying along with the retry count,

_eventAggregator.GetEvent<WaitAndRetryEvent>().Publish(retryCount);

and the ViewModel will update the text as it is subscribed to this event.

void OnRetrying(int retryCount)
{
    // if retrying for the second time, let the user know politely
    if (retryCount > 1)
    {
        LoadingText = "Still loading...";
    }
}

Doing so, we politely notify the user that their data is being loaded.

What’s Next

If you are interested in learning more about other Polly features such as,

  • PolicyRegistry
  • Combined policies
  • Circuit Breaker
  • Cache
  • Fallback

visit Polly to learn more.

Let me know in the comments if this post was interesting to you. Enjoy 😊

Resources

Full sample source: https://github.com/hnabbasi/polly-mvvm
Cloud icon made by Freepik from www.flaticon.com

Advertisement

4 thoughts on “Enhance Your Xamarin.Forms App’s User Experience With Polly

  1. Mike Qu says:

    Hey Hussain, great post! So I recently implemented this myself after reading this, but found out that many of the actual transient network errors are platform-specific (mostly Android/Mono I believe).

    Things like Java.Net socket closed, operation cancelled, unknown host name, connection abort, etc.

    Because our API service codes are typically inside a .NET standard project that has no dependency on the native Android or iOS project, we can’t quite categorize this type of errors cleanly via Polly’s Handle policy. A catch-all “Exception” is obviously problematic too as we don’t really want to retry non-retryable issues.

    What I ended up doing is that in addition to the standard HttpClient exceptions, I added a check for exception messages containing some of those keywords I encountered. This is obviously kind of hacky. I wonder if you have any other ideas.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.