Aka Awesome Refactored
In the last post we talked about Akavache and frankly how awesome it is. In this post we’re going to take a simple Xamarin Forms application which uses a “home grown” caching mechanism and refactor it into one which uses Akavache. We’ll see how much simpler and more elegant Akavache makes our code and also check out some design considerations for using Akavache along the way.
Why Refactor?
We already have a working application, why even bother refactoring in the first place? Plain and simply, why should we reinvent the wheel? If we can swap out our (probably buggy) code for a framework which has been tested by the community, has more features, and has been thought out and designed for the sole purpose of caching – we’d be dumb not to use it.
Plus when integrated, Akavache makes our code cleaner and more reliable … and who doesn’t like that?
What We’re Refactoring
The quick overview:
In the Cold Hard Data Caching post, we wrote a Xamarin Forms application that downloaded questions from StackOverflow tagged with the keyword Xamarin and displayed those questions in a ListView. Tapping on a question displayed another page with either the answer or a ‘no answer found’ placeholder. We used a homegrown caching mechanism in the previous version and we’ll be upgrading to Akavache.
The detailed specs:
- Xamarin Forms app
- Show a ListView of dates for the last 14 days
- Tapping on a day shows a ListView of questions from StackOverflow from that day tagged with “Xamarin”
- Questions should be cached when downloaded
- Stack Overflow should be queried for any new questions asked for a given day since the last time the cache was updated
- Expire questions from cache in 7 days
- Show date and time when question was downloaded from StackOverflow
- When question row tapped: download, cache and display an answer on new screen. There will only ever be a single answer per question, so if an answer is found in cache, no need to check internet
Let’s Refactor!
If you’d like to follow along with the code, you can find the 2 solutions on GitHub here:
Before refactoring: StackCache
After refactoring: AkaAwesome
First let’s prep our existing project to use Akavache.
- While not required, we can get rid of the SQLite.Net references. Obviously you would only do this if you’re not using SQLite.NET elsewhere in your app.
- Next, we can remove any classes whose specific purpose is to perform SQLite database management. Such as a class which inherits from a SQLiteConnection object or manages the connection. Also, any platform specific code which sets things such as the location of the database file can be removed as well.
- Finally we’re going to add Akavache via NuGet. If you remember from the last post, first we want to add the Akavache.SQLite3 package, and then the Akavache package, in order for the dependencies to resolve properly.
Now let’s look at actually doing the refactoring. Our app is made up of 3 pages:
- Page with ListView of dates from previous 2 weeks
- Page with ListView of questions from each of those dates
- Page with a given answer’s detail for each of those questions
There’s no caching involved in the first screen, no refactoring there. But there’s definitely some with the next 2 screens. Let’s look at the easier of the two first.
Answer Detail Screen Refactor
This page receives an integer representing the Question’s Id, and then uses that to either load it from the cache, or download it from StackOverflow.
The original method to load/display the answer looks like this:
protected async Task LoadAnswer (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 truly show how much code there is involved here, below are the 2 database functions invoked to support the caching:
public async Task<AnswerInfo>GetAnswerForQuestion (int questionId)
{
return await Table<AnswerInfo> ().Where (ai => ai.QuestionID == questionId).FirstOrDefaultAsync ().ConfigureAwait (false);
}
public async Task SaveAnswer (AnswerInfo answer)
{
int answerId = answer.AnswerID;
var dbRecord = await Table<AnswerInfo> ()
.Where (ai => ai.AnswerID == answerId)
.FirstOrDefaultAsync ().ConfigureAwait (false);
if (dbRecord == null)
await InsertAsync (answer).ConfigureAwait (false);
else
await UpdateAsync (answer).ConfigureAwait (false);
}
That’s a fair amount of code, and a fair amount of room to introduce bugs … Let’s add some Akavache awesomeness to make it a whole lot more succinct.
Design Considerations
Since Akavache is a key/value store – we first must consider what we want to use for our key and what we want to use for our value.
In this case that’s pretty straight forward. The answer retrieval from StackOverflow is based on passing in a QuestionId, it only makes sense to use that as our key. Likewise, the value portion is straight forward as well, we’ll just put the AnswerInfo object that we’re displaying in there.
Next we have to figure out which of the functions that Akavache provides that we’ll want to make use of. GetOrFetchLatest
seems like an ideal candidate here. That function will either pull the object out of the cache, or execute a passed in function whose responsibility it is to return an object. Akavache will then put that object into the cache with the corresponding key originally passed int.
Let’s take a look at our refactored code:
protected async Task LoadAnswer (int questionId)
{
AnswerInfo currentAnswer = await BlobCache.LocalMachine.GetOrFetchObject<AnswerInfo>(
_questionId.ToString(),
async() => await new StackOverflowService().GetAnswerForQuestion(questionId),
DateTime.Now.AddDays(7)
);
if (currentAnswer != null) {
_theAnswer.AnswerID = currentAnswer.AnswerID;
_theAnswer.QuestionID = currentAnswer.QuestionID;
_theAnswer.AnswerBody = currentAnswer.AnswerBody;
} else {
// Nothing found on the web or in the cache - so invalidate the cache - don't want null stored
await BlobCache.LocalMachine.InvalidateObject<AnswerInfo> (_questionId.ToString ());
_theAnswer.AnswerBody = "No answer found on StackOverflow or in cache";
}
}
That’s it. All of our original code is refactored just into those couple of lines. The database CRUD operations and invocation of the API to download the StackOverflow answer is taken care of by Akavache in lines 4-8. The rest of the function just takes care of display.
One thing to note is that we immediately call InvalidateObject
if the answer from StackOverflow API comes back as null. This deletes the key/value pair from the cache. This way we don’t store a null value with the key in the cache, thus making sure the key doesn’t exist in the cache and we always invoke the passed in function.
Question List Screen Refactor
This page receives a date value which it then uses to query the cache or StackOverflow for the questions asked on that date.
In addition, on this page we’re also always downloading the questions from a given day and adding “new” questions to the display and also to the cache. This way if the user views the questions at noon, and then again at 7 PM, they’ll get all the questions asked in the interim.
The original function to load and display the questions:
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 (_dateToDisplay);
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 (_dateToDisplay);
// 5. See which questions are new from web and only add those to the display and cache
foreach (var question in downloadedQuestions) {
if (_displayQuestions.Contains (question) == false) {
_displayQuestions.Insert (0, question);
// 6. Save the new question to the cache
await App.StackDataManager.Database.SaveQuestion (question);
}
}
} catch (NoInternetException) {
await HandleException ();
}
}
The database functions which provide the caching:
public async Task<IList<QuestionInfo>>GetQuestions (DateTime dateToDisplay)
{
var topDate = dateToDisplay.AddDays (1);
return await Table<QuestionInfo> ().Where (qi =>
qi.InsertDate > dateToDisplay &&
qi.InsertDate < topDate
).ToListAsync ().ConfigureAwait (false);
}
public async Task SaveQuestion (QuestionInfo question)
{
int questionId = question.QuestionID;
var dbRecord = await Table<QuestionInfo> ()
.Where (qi => qi.QuestionID == questionId)
.FirstOrDefaultAsync ().ConfigureAwait (false);
if (dbRecord == null) {
question.InsertDate = DateTime.Now;
await InsertAsync (question).ConfigureAwait (false);
} else {
question.InsertDate = dbRecord.InsertDate;
await UpdateAsync (question).ConfigureAwait (false);
}
}
public async Task DeleteQuestionsAndAnswers ()
{
DateTime cutOff = DateTime.Now.AddDays (-7);
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);
}
}
Wow – that’s a lot of code … and a lot of places for bugs to creep in! Let’s sprinkle some Akavache awesomeness on it!
Design Considerations
Again we need to decide on what we’re going to use for our key/value pair. Since the page is getting a date in the constructor, it seems natural to use that as the key. In this case, we’re going to use the Ticks value of date.
As for the value – again straight forward. We’re displaying a list of QuestionInfo objects, may as well store a List<QuestionInfo>
as the value!
Since we need to not only grab data from the cache if it exists, but also always retrieve the latest data and add it to the cache, we should use the GetAndFetchLatest
function.
If you remember from the previous post, this function will return anything in the cache matching the passed in key. It also always executes the passed in function to get the latest data. It will then update the cache and return that updated data. That means this function can return more than once, so we cannot await
it, rather we’ll have to employ the Subscribe
function.
All of the code above refactors into the following:
protected void LoadQuestions ()
{
BlobCache.LocalMachine.GetAndFetchLatest<IList<QuestionInfo>> (
_dateToDisplay.Ticks.ToString (),
async () => await new StackOverflowService ().GetQuestions (_dateToDisplay),
null,
_dateToDisplay.AddDays (7)
).Catch (Observable.Return (new List<QuestionInfo> ())).Subscribe (
returnedQuestions => {
Device.BeginInvokeOnMainThread (() => DisplayQuestions (returnedQuestions));
}
);
}
private void DisplayQuestions (IList<QuestionInfo> questions)
{
foreach (var item in questions) {
if (!_displayQuestions.Contains (item)) {
_displayQuestions.Insert (0, item);
}
}
}
Seriously, that’s all there is. All of the plumbing to implement caching from the first example has been whittled down to just a couple of lines.
The Subscribe
function gets called every time the underlying observable collection (in this case a list of QuestionInfo objects) gets updated. From there, we update the UI.
Database, function to invoke StackOverflow, error handling, UI updating – all together.
Talk about elegant!
Conclusion
When we implement caching in our applications, we have to write a lot of code to implement it that has nothing to do with the core functionality of our application. That could introduce a lot of bugs. However, by using a framework such as Akavache to perform the caching for us, we are able to reduce the amount of code significantly and also reduce the possibility of bugs significantly as well. Not only that – our code becomes much more succinct and elegant as well.