Skip to main content Skip to footer

A deeper look at the Medal Tracker architecture

In our last blog, we took a quick look at the Xamarin.Forms Medal Tracker sample. The Medal Tracker app uses Xuni, live data, and data caching to provide an interesting way to illustrate the results of the Rio 2016 Olympics. In this article, we’ll take a deeper look into Medal Tracker architecture.

Getting Web Data

The Medal Tracker sample uses a combination of JSON.NET and Microsoft's HTTP Client libraries to retrieve and deserialize data. We've covered these libraries in the past, but we'll detail exactly how they're used for this sample. By creating a model for the data that's used throughout the app, we can use JSON.NET to deserialize our json objects that we'll retrieve using Microsoft's HTTP libraries. The model is very simple in this case.



public class OlympicCountry : INotifyPropertyChanged  
{  
public int rank { get; set; }  
public string name { get; set; }  
public string imageUrl { get; set; }  
public int gold { get; set; }  
public int silver { get; set; }  
public int bronze { get; set; }  
public int total { get; set; }  

public event PropertyChangedEventHandler PropertyChanged;  

private void OnPropertyChanged(string propertyName)  
{  
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));  
}  
protected void OnPropertyChanged(PropertyChangedEventArgs e)  
{  
if (PropertyChanged != null)  
PropertyChanged(this, e);  
}  
}  


The model will be used to represent the json we're pulling down from web via an async Task. All that's really done by the sample is to read the web data in asynchronously, use JSON.NET to deserialize the data into a list of OlympicCountry objects, and the return the list.


public async Task<List<OlympicCountry>> AsyncGetWebData()  
{  
OlympicList = new List<OlympicCountry>();  
HttpClient httpClient = new HttpClient();  
Task<string> stringAsync = httpClient.GetStringAsync("http://demos.wijmo.com/5/angular/OlympicTracker/OlympicTracker/data/2016Data.txt");  
string result = await stringAsync;  
OlympicList = JsonConvert.DeserializeObject<List<OlympicCountry>>(result);  

return OlympicList;  
}  

Elsewhere in the sample, this method (and returned list) is used to populate the ItemsSource of both the chart and grid. Since the data is continuously being updated on the web, the app uses a timer to occasionally refresh the ItemsSources of the controls.


var minutes = System.TimeSpan.FromMinutes(60);  
Device.StartTimer(minutes, () =>  
{  
    //RefreshPages resets the ItemsSources of the controls among other things  
    //see project for full method  
RefreshPages();  
return true;  
});  

Caching Results

The Medal Tracker sample has a few contingencies to handle cases where there is no internet connection for a device. There are a couple of approaches for providing local data as a cache in these cases. Xamarin.Forms allows you to package a file into the PCL which works as a read only way of providing some backup data. The file mirrors the layout of the fetch web data, and is packaged into the PCL.


OlympicList = new List<OlympicCountry>();  
var assembly = typeof(WebData).GetTypeInfo().Assembly;  
Stream stream = assembly.GetManifestResourceStream("OlympicTracker.Cache.2016Data.txt");  
string text = "";  
using (var reader = new System.IO.StreamReader(stream))  
{  
text = reader.ReadToEnd();  
}  
OlympicList = JsonConvert.DeserializeObject<List<OlympicCountry>>(text);  

This file is read only though, and we wouldn't be able to update it with more recent results over time. We can also use a dependency service to create a local copy of the data which the app can update each time it successfully fetches data. That way if we've fetched updated results, but lose connection to the internet, we can still load relatively recent data. We've covered DepencyServices in depth elsewhere, but we'll reiterate the process here too. The PCL has an interface called ISaveAndLoad.


namespace OlympicTracker  
{  
public interface ISaveAndLoad  
{  
Task SaveFile(string contents);  
Task<string> LoadFile();  
bool FileExists();  
}  
}  

The interface is then implemented for each platform since each handles their file systems a bit differently. On Android, you'll see the following implementation:


[assembly: Dependency(typeof(SaveAndLoad_Android))]  
namespace OlympicTracker.Droid  
{  
public class SaveAndLoad_Android : ISaveAndLoad  
{  
public async Task SaveFile(string content)  
{  
var path = CreatePathToFile("2016Data.txt");  
using (StreamWriter sw = File.CreateText(path))  
await sw.WriteAsync(content);  
}  

public async Task<string> LoadFile()  
{  
var path = CreatePathToFile("2016Data.txt");  
using (StreamReader sr = File.OpenText(path))  
return await sr.ReadToEndAsync();  
}  

public bool FileExists()  
{  
return File.Exists(CreatePathToFile("2016Data.txt"));  
}  

string CreatePathToFile(string filename)  
{  
var docsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);  
return Path.Combine(docsPath, filename);  
}  
}  
}  

On iOS, you'll see something slightly different:


[assembly: Dependency(typeof(SaveAndLoad_iOS))]  
namespace OlympicTracker.iOS  
{  
public class SaveAndLoad_iOS : ISaveAndLoad  
{  
public static string DocumentsPath  
{  
get  
{  
var documentsDirUrl = NSFileManager.DefaultManager.GetUrls(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User).Last();  
return documentsDirUrl.Path;  
}  
}  

public async Task SaveFile(string content)  
{  
string path = CreatePathToFile("2016Data.txt");  
using (StreamWriter sw = File.CreateText(path))  
await sw.WriteAsync(content);  
}  

public async Task<string> LoadFile()  
{  
string path = CreatePathToFile("2016Data.txt");  
using (StreamReader sr = File.OpenText(path))  
return await sr.ReadToEndAsync();  
}  

public bool FileExists()  
{  
return File.Exists(CreatePathToFile("2016Data.txt"));  
}  

static string CreatePathToFile(string fileName)  
{  
return Path.Combine(DocumentsPath, fileName);  
}  
}  
}  

To fully implement the caching behavior we'll need to do a few things. When the app tries to refresh it's data, we'll need to check these conditions:* If the data is successfully fetched from the web, write the data to the cache

  • If the data is not successfully fetched from the web, read data from the local file system cache
  • If the file system cache doesn't exist, load the default data from the PCL cache Thus the code looks something like: ~~~

//see project for full implementation
List olist;
try
{
//get web data and write it to local cache
olist = await wd.AsyncGetWebData();
await DependencyService.Get().SaveFile(JsonConvert.SerializeObject(olist));

}
catch
{
//alert that fetching webdata failed
await DisplayAlert("No Internet Connect", "No Internet connection found. Loading cached Data.", "OK");
//if the cache file exists load it
if (DependencyService.Get().FileExists() == true)
{
string result = await DependencyService.Get().LoadFile();
olist = JsonConvert.DeserializeObject<List>(result);
}
//if not load from the PCL file
else {
//read local data in PCL
olist = wd.GetCache();
}
}

~~~

Wrap up

The Medal Track architecture isn't overly complex, but it does employ a few tricks. This type of fetching and caching ensures that the sample remains functional regardless internet connection, and that the data remains as up to date as possible given any circumstance.

Download the MedalTracker sample >>

MESCIUS inc.

comments powered by Disqus