In this exercise, we will add more resiliency to the app, so we can handle situations with low or no connectivity.
- Use Refit to get rid of boilerplate code
- Make sure that we use the native HttpClient stack
- Check for connectivity with Xamarin.Essentials
- Add a retry mechanism with Polly
- Add a background job to sync data
The ConferenceApiService
currently uses HttpClient
directly to consume the conference API. While this works fine, this will lead to a large amount of repeated boilerplate code when you start to implement more and more methods who need to create and send the HTTP request, check the status code and deserialize the response. The Refit
Nuget package can help us get rid of all this boilerplate code by offering a generated typed client for API's.
-
Add the
Refit
Nuget package to theConferenceApp
,ConferenceApp.iOS
andConferenceApp.Android
projects. -
Add a new interface named
IConferenceApi
to theServices
folder in theConferenceApp
project:using System.Collections.Generic; using System.Threading.Tasks; using ConferenceApp.Contracts.Models; using Refit; namespace ConferenceApp.Services { [Headers("Accept: application/json")] public interface IConferenceApi { [Get("/sessions")] Task<IEnumerable<Session>> GetSessions(); [Get("/sessions/{sessionId}")] Task<Session> GetSession(string sessionId); [Get("/speakers")] Task<IEnumerable<Speaker>> GetSpeakers(); [Get("/speakers/{speakerId}")] Task<Speaker> GetSpeaker(string speakerId); } }
The
[Get(...)]
attributes are markers forRefit
so that it can map the interface methods to a REST API's endpoints. Note that you can also setHeaders
at the interface level, which will be sent with each request.Refit
also offers extension points to inject authentication headers. -
In the
Services/Startup.cs
file, add the following private fields to theStartup
class:private const string BASE_URI = @"https://conferenceapp-demo.azurewebsites.net/api"; private static readonly HttpClient httpClient = new HttpClient { BaseAddress = new System.Uri(BASE_URI) };
Note that it is recommended practice to reuse the instance of an
HttpClient
. Therefore, we can instantiate it as a static field. -
In the
ConfigureServices
method, add the registration of the generatedRefit
client for theIConferenceApi
interface:services.AddSingleton(p => RestService.For<IConferenceApi>(httpClient));
We can now consume the generated client in the ConferenceApiService
:
```csharp
public class ConferenceApiService : IConferenceApiService
{
// take a depencency on the IConferenceApi interface
private readonly IConferenceApi conferenceApi;
public ConferenceApiService(IConferenceApi conferenceApi)
{
this.conferenceApi = conferenceApi;
}
public async Task<IEnumerable<Session>> DownloadConferenceData(CancellationToken cancellationToken)
{
return await conferenceApi.GetSessions().ConfigureAwait(false);
}
}
```
Test the app and try the SYNC
button on top of the SessionsPage
. It should still execute the call to the Web Api.
When you create a new Xamarin.Forms app, the default values for the HTTP client stack project settings are set to native. However, in an older app, this might not be the case. Therefore, it is always good to check whether these settings are correct.
The native HTTP client stack is preferred for performance and security reasons.
- Go into the project settings of the Android project
- Under Android Build, verify the values of the
HttpClient implementation
andSSL/TLS implementation
These should be:
AndroidClientHandler
andDefault (Native TLS 1.2+)
- Go into the project settings of the iOS project
- Under Android Build, verify the value of the
HttpClient implementation
setting
This should be:
NSUrlSession (iOS 7+)
We recommend verifying these settings your existing Xamarin projects at your own office.
Xamarin.Essentials
offers a cross platform Connectivity
class, which we can use to determine the current network connectivity. Before we call the Web API, we should check whether the device is online, so that we don't waste resources on an HTTP call which is going to fail.
Xamarin.Essentials
is already added to the application, so we don't have a add any new Nuget packages.
-
In
Services\SyncService.cs
, find theSyncConferenceData
method -
Add the following condition before the call to
conferenceService.DownloadConferenceData()
:if (Connectivity.NetworkAccess == NetworkAccess.Internet) { // ... }
-
For Android, we need to add a permission request to the Android manifest:
AccessNetworkState
. Make sure that it is added:<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
This simple check will prevent us from trying to call the Web API when it is offline. You can use the Connectivity
class anywhere in your Xamarin app, and also subscribe to changes to the network condition.
Any network call done by an application poses many risks. We need to prepare for things going wrong. One of the things we can do is add an automatic retry mechanism in case we encounter a transient error (an error which might be gone when tried at a later moment).
Instead of building our own retry loops or circuit breakers, we can leverage the Polly
Nuget package to do that for us, so we can focus on the business logic.
-
Add the
Polly
Nuget package to theConferenceApp
project -
In
ConferenceApiService.cs
, add a new field to theConferenceApiService
class:// Handles ApiExceptions with Http status codes >= 500 (server errors) and status code 408 (request timeout) private readonly AsyncRetryPolicy transientApiErrorPolicy = Policy .Handle<ApiException>(e => (int)e.StatusCode >= 500) .Or<ApiException>(e => e.StatusCode == HttpStatusCode.RequestTimeout) .WaitAndRetryAsync ( retryCount: 3, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) );
This defines a retry policy which triggers in case of a transient error (any server error with a status code >
500
, or in case of a request timeout - status code408
). It will retry up to 3 times, with an increasing timespan between the retries. -
Wrap the call to
conferenceApi.GetSessions()
in the retry policy:return await transientApiErrorPolicy .ExecuteAsync(async () => { Debug.WriteLine("Trying service call..."); return await conferenceApi.GetSessions().ConfigureAwait(false); });
The SYNC
feature should still work as expected.
You can trigger the retry policy by adding a parameter to the GetSessions
method of the IConferenceApi
interface:
```csharp
[Headers("Accept: application/json")]
public interface IConferenceApi
{
[Get("/sessions?injectError=true")]
Task<IEnumerable<Session>> GetSessions();
//...
}
```
The API will now return a 500 Internal Server Error
with every call to the /sessions
endpoint. You should then see that the retry policy retries the call 3 more times, before giving up.
Don't forget to remove the
injectError=true
parameter from theIConferenceApi
interface!
Ideally, data in the app should already be up-to-date before the user opens the app. Both iOS and Android provide mechanisms to build background jobs that perform certain tasks such as downloading new content from an API. These mechanisms are very different per platform though.
The Shiny.Core
Nuget library offers us a cross platform way to build these background jobs, and they hook into the existing mechanisms in the operating system.
Shiny.Core
is already installed in the application, since we're using its dependency injection framework. The Shiny.Jobs
framework leverage this depencency injection framework too.
Adding a new job is now rather simple:
-
Create a new class called
BackgroundSyncService
in theServices
folder of theConferenceApp
project:using System; using System.Threading; using System.Threading.Tasks; using Shiny.Jobs; namespace ConferenceApp.Services { public class BackgroundSyncJob : IJob { private readonly ISyncService syncService; public BackgroundSyncJob(ISyncService syncService) { this.syncService = syncService; } public async Task<bool> Run(JobInfo jobInfo, CancellationToken cancellationToken) { Console.WriteLine("Syncing conference data in the background..."); var result = await syncService.SyncConferenceData(cancellationToken); Console.WriteLine($"Finished with result: {result}"); jobInfo.Repeat = true; return result; } } }
This class implements the
IJob
interface, which we can plug in theShiny
framework. It will then be scheduled automatically when registered. -
In
Services\Startup.cs
in theConferenceApp
project, register the new background job in the depencency injection container:services.RegisterJob(new Shiny.Jobs.JobInfo { Identifier = nameof(BackgroundSyncJob), Type = typeof(BackgroundSyncJob), RequiredInternetAccess = Shiny.Jobs.InternetAccess.Any, BatteryNotLow = true, Repeat = true });
However, these background jobs have a few prerequisites to make the operating systems honor the request to run in the background.
On iOS, Shiny
needs to hook into the PerformFetch
lifecycle method of the AppDelegate
class.
-
Override the
PerformFetch
method in theAppDelegate
class:public override void PerformFetch(UIApplication application, Action<UIBackgroundFetchResult> completionHandler) { JobManager.OnBackgroundFetch(completionHandler); }
Whenever iOS calls this method, it will be delegated to
Shiny
's job manager. -
In the
info.plist
file, make sure thatEnable Background Modes
is selected, and thatBackground fetch
is enabled:
iOS should now run the background job whenever it calls performFetch
. It will do so at its own discretion, depending on network connectivity, battery level and how well-behaved the app is.
On Android, we need to make sure that the app is allowed to monitor the battery level and that it is informed when the system (re)boots, so that it can start the background job.
-
In the Android Manifest, make sure that the following permissions are present:
<uses-permission android:name="android.permission.BATTERY_STATS" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />