Cold Hard Data Cache - Or Saving Our Users Money!

After that last post on SQLite, let’s explore one of the best things we can do with persisted data for the users of our mobile applications … provide a cache of data. And even better … the data cache that we’re going to give them is going to save them hard earned money!

Alright – not even I can really extend the metaphor of data caching to saving money, at least not without really stretching, so let’s just dive right into data caching, shall we?

What Is Caching & Why Is It Important?

This probably goes without saying, but when I talk about caching data within a mobile application, I am talking about saving data downloaded from the internet to the mobile device for later use. This data is not necessarily meant to be kept around forever, but having it on the device will make the user experience much more enjoyable.

There are two situations where this “more than fleeting, but not permanent data” comes in very useful.

1) Perceived performance. If we know what data our users will want to see next, we can download it before they want it. Our app will then seem much faster because the data is already there; loading it from a local cache is obviously much faster than downloading it from the internet.

2) Offline resilience. Users never stay within network reach. They’re out there climbing mountains, sitting on the beach, or hurtling through the air in an aluminum tube. But no matter what, they have come to expect that the app should still work, even when they’re offline. Providing a cache of data will at least all the app to maintain its state the last time it was connected to the internet.

What Data Should We Cache?

Of course, some types of data are tailored made for caching, while other types make no sense at all. Any data which does not change very often, or is not very time sensitive is a good candidate for caching. Data which changes often, stock prices or the weather, do not fit well into a caching model.

Any data which is not overly time sensitive is a good candidate to be cached

Examples of good data to cache:

  • Twitter stream
  • Product data from a retailer
  • RSS feed
  • News stories
  • Plane or concert tickets
  • Anything of historical nature

How Do We Cache In Xamarin Apps?

I thought you’d never ask! There a couple of ways we can go about this, but in this post we’ll create our own caching logic with SQLite as the backend. In a future post, we’ll refactor the project to use Akavache, a library created by Paul Betts that does a lot of the work (and I do mean a lot) for us. But before we run, we have to walk, and it’s best to understand how everything fits together first.

The Project

We’re going to build an application that downloads questions and answers from StackOverflow that are tagged with “Xamarin”. The app will then be able to scroll through a list of all the questions. It will allow a user to tap on a question’s row, and then view an answer for that question. A simple navigation page Xamarin Forms app.

The data caching part comes in to play that we’ll be saving both the questions and answers to a SQLite database, so they will be available for offline use. (That way when we’re doing development in the deepest, darkest reaches of a coffee shop where no wifi signal has ever been encountered … we’ll still able to get our StackOverflow questions & answers)!

The way the StackOverflow API works (or at least the way we’re using it here), we first have to download questions, then download the answers. So, to increase perceived performance with data caching, we’ll grab the answers in the background after the questions have been downloaded. This will give the user the impression of a fast load time – because we know what they want to view even before they do!

So that’s how we’ll use data caching in this app. But first, let’s take a look at the pre-cache version…

Before Caching

Loading the StackOverflow questions into the ListView looks something like this:

protected async Task LoadQuestions() {
    _displayQuestions.Clear ();
 
    var questionAPI = new QuestionService ();
 
    var downloadedQuestions = await questionAPI.GetQuestions ();
 
    foreach (var question in downloadedQuestions) {
        _displayQuestions.Add (question);
    }                
} 

Pretty simple and straight forward. All we’re doing is calling out to a class which encapsulates a web service and adding the return to an ObservableCollection named _displayQuestions.

In the same way, when we load up the answer to be shown, we do the following:

protected async Task LoadAnswers (int questionId)
{                
    var answerAPI = new StackOverflowService ();
            
    var downloadedAnswer = await answerAPI.GetAnswerForQuestion (questionId);
 
    if (downloadedAnswer != null) {                
        _theAnswer.AnswerID = downloadedAnswer.AnswerID;
        _theAnswer.QuestionID = downloadedAnswer.QuestionID;
        _theAnswer.AnswerBody = downloadedAnswer.AnswerBody;
    } else {
        _theAnswer.AnswerBody = "No answer found";
    }        
}

Adding The Cache

Now let’s add some cache to the app! The entire project can be found on GitHub here.

At a high level, caching involves the following:

  • Downloading data from the network
  • Saving the downloaded data to the cache
  • Deleting any data no longer needed from the cache
  • Displaying a combination of data downloaded and loaded from the cache

The biggest thing to remember here is that there is no one correct way to implement the points above. The business logic of your application will decide that for you.

Downloading the data

This really isn’t any different than what we looked at above when we didn’t have caching in place. However, one thing to note is that we should check for network connectivity and be explicit in communicating the results back to the user. This way the user is fully informed as to whether the data coming back is “fresh” from the internet, or “stale” from our cache.

James Montemagno has a great cross platform plugin to check connectivity called, of all things, Connectivity Plugin it’s really worth checking out, and is on NuGet as well.

Saving, deleting and displaying the data

The next 3 points from above will all get smooshed together for illustration purposes.

But this is where we get to use some of the techniques we learned last week in the SQLite post!

The first thing I want to look at is our data persistence. Below are 3 functions which perform the basic CRUD operations on our models. Nothing special going on here at all. If anything – the only fun thing is that we’re considering anything older than 2 days too old to be in the cache … technology! It moves at the speed of light!

public async Task<IList<QuestionInfo>>GetQuestions() 
{
    return await Table<QuestionInfo> ().ToListAsync ().ConfigureAwait (false);
} 
 
public async Task SaveQuestions(IList<QuestionInfo> questions)
{
    foreach (var item in questions) {
        int questionId = item.QuestionID;
 
        var dbRecord = await Table<QuestionInfo> ()
            .Where (qi => qi.QuestionID == questionId)
            .FirstOrDefaultAsync ().ConfigureAwait (false);
 
        if (dbRecord == null)
            await InsertAsync (item).ConfigureAwait (false);
        else
            await UpdateAsync (item).ConfigureAwait (false);
    }
} 
 
public async Task DeleteQuestionsAndAnswers()
{
    DateTime cutOff = DateTime.Now.AddDays (-2);
 
    var oldQuestions = await Table<QuestionInfo> ().Where (qi => qi.InsertDate < cutOff).ToListAsync ().ConfigureAwait (false);
 
    foreach (var item in oldQuestions) {
        var questionId = item.QuestionID;
 
        var oldAnswers = await Table<AnswerInfo> ().Where (ai => ai.QuestionID == questionId).ToListAsync ().ConfigureAwait (false);
 
        foreach (var oa in oldAnswers) {
            var answerId = oa.AnswerID;
            await DeleteAsync<AnswerInfo> (answerId);
        }
 
        await DeleteAsync<QuestionInfo> (questionId);
    }
}

Where the fun starts to happen in the next section below, this is where we tie everything together.

  1. Delete anything in the cache that’s too old to display
  2. Load up the remaining cached items from the database
  3. Download any new questions from StackOverflow
  4. Populate the user interface with those questions
  5. In the background, save the downloaded questions to the cache (for offline resilience)
  6. In the background, download any answers to the questions (for perceived performance)

And that’s exactly what’s happening in the code below:

protected async Task LoadQuestions ()
{
    _displayQuestions.Clear ();
 
    // 1. Get rid of anything too old for the cache
    await App.StackDataManager.Database.DeleteQuestionsAndAnswers();
 
    // 2. Load up cached questions from the database
    var databaseQuestions = await App.StackDataManager.Database.GetQuestions ();
 
    foreach (var item in databaseQuestions) {
        _displayQuestions.Add (item);
    }
 
    try {
        // 4. Load up new questions from web
        var questionAPI = new StackOverflowService ();
        var downloadedQuestions = await questionAPI.GetQuestions ();
        var newQuestionIDs = new List<int>();
 
        foreach (var question in downloadedQuestions) {
            if (_displayQuestions.Contains (question) == false) {
                _displayQuestions.Insert (0, question);
                newQuestionIDs.Add(question.QuestionID);
            }
        }
 
        await App.StackDataManager.Database.SaveQuestions (downloadedQuestions);
 
        if (newQuestionIDs.Count > 0)
            await GrabAnswers(newQuestionIDs);
 
    } catch (NoInternetException) {
        await HandleException ();
    }
}
 
// 5. Proactively grab the answer for the questions
protected async Task GrabAnswers(List<int> questionIDs)
{           
    var soAPI = new StackOverflowService ();
 
    var lotsOfAnswers = await soAPI.GetAnswersForManyQuestions (questionIDs);
 
    await App.StackDataManager.Database.SaveAnswers (lotsOfAnswers);
}

To indicate whether a question was loaded from the database or grabbed from the web, I’m prefixing the title of the question with a “D” (for database) or “W” (for web) to indicate to the user where it came from in the model class. Not pretty, not elegant – but it gets the point across.

public string TitleWithLoadFrom {
    get {
        if (LoadedFromWeb)
            return "W - " + this.Title;
        else
            return "D - " + this.Title;
    }
}

Then for the sake of completeness, here is what the LoadAnswer function turned into when caching was added. It first checks the database, then the web, then it gives up and says it can’t find anything.

You may be wondering why we’re checking the database first and not going out to the web and seeing if there are any new answers to display. And why are we only displaying one answer?

That gets back to the point I made above, there is no one correct way to implement data caching, the business requirements for your app will decide that for you. And the business requirements for this app were to get it out the door ASAP and to make this point while doing it!

protected async Task LoadAnswers (int questionId)
{   
    AnswerInfo currentAnswer = null;
 
    // 1. Load from the database
    currentAnswer = await App.StackDataManager.Database.GetAnswerForQuestion (questionId);
 
    if (currentAnswer != null) {
        _theAnswer.AnswerID = currentAnswer.AnswerID;
        _theAnswer.QuestionID = currentAnswer.QuestionID;
        _theAnswer.AnswerBody = currentAnswer.AnswerBody;
    } else {
        // 2. No database record... Load answer from the web            
        var answerAPI = new StackOverflowService ();
 
        var downloadedAnswer = await answerAPI.GetAnswerForQuestion (questionId);
 
        if (downloadedAnswer != null) {             
            _theAnswer.AnswerID = downloadedAnswer.AnswerID;
            _theAnswer.QuestionID = downloadedAnswer.QuestionID;
            _theAnswer.AnswerBody = downloadedAnswer.AnswerBody;
 
            // 3. Save the answer for next time
            await App.StackDataManager.Database.SaveAnswer (_theAnswer);
 
        } else {                    
            _theAnswer.AnswerBody = "No answer found";
        }
    }
}

To indicate to the user whether the answer came from the cache or the web, I have a label on the screen which says database or web on the page. It is data bound to a property whose change event fires when AnswerBody gets changed. Again, not elegant, but it gets the point across.

public string AnswerBody {
    get { return answerBody; }
    set{
        if(answerBody != value) {
            answerBody = value;
            OnPropertyChanged ("AnswerBody");
            OnPropertyChanged ("LoadedFromText");
        }
    }
}
 
[Ignore]
public string LoadedFromText {
    get {
        return LoadedFromWeb ? "Loaded From Web" : "Loaded From Database";
    }
}

You can view the full code on GitHub.

Summing It Up

Really, when you think about it, providing a data cache for your users, whether it be to enable offline access of the app, or to provide a perceived performance increase, is just plain nice to do! In fact, it really should be expected for any app that downloads any sort of data from the internet.

The big things that you’re going to want to remember about caching are:

  1. Caching offers both perceived performance gains and offline resiliency to your application.
  2. Only certain types of data are ideal for caching, data that’s not very time sensitive.
  3. The specifics of how you implement the caching will vary greatly from situation to situation.