Tuesday, 1 December 2009

iPhone Offline Web Apps – the RESTful Way – Part 2 of 2

html5REST [Concrete/Very Interesting] Ok, so hopefully you got the static application working offline. I was quite satisfied when I got it working, though not nearly as much fun as watching Push in HD on my Samsung 6000 LED TV (thinner than my laptop – wow).

Now for the RESTful part. We need to make REST calls when online and work from a cache when offline.

 

 

 

 

 

Cache Manifest White list

As well as defining the resources that are cached, you can specify resources that should not be cached in your manifest file:

CACHE MANIFEST
# Version 0.9.4.1129.007
 
NETWORK:
/iretailpassportsvc/clientservices.svc
 
CACHE:
index.htm
images/arrow.png

Any resources named under the ‘NETWORK:’ label are considered white listed and should not be fetched from the cache.

Notice that you can name cached resources after a ‘NETWORK:’ section by using the ‘CACHE':’ label.

Naming white listed resources allows us to access resources in our web app that are not in the cache. This solves a serious problem for us. Imagine trying to add your first service call to an offline application with static only resources. You can’t name the service in the manifest as a cacheable resource. This wouldn’t work the first time you went offline and the app would fail to load with a ‘Internet not available’ error. If you omit the resource, the manifest is wrong and you will end up running the cached app, the one without the service call. Real bummer.

‘NETWORK:’ specifies a non-cached resource and one without fallback. There is a ‘FALLBACK:’ label that allows us to specify a non-cached resource and cached alternative. Useful if you want to put up a big banner when offline saying ‘Sorry, come back later’ but not what we are trying to do here.

One final point about the white list before I move on. Unlike the named cached resources, the white list is in fact a resource prefix. So, in the example above, I named my REST service, not the individual methods. This is useful because our methods are likely to have parameters and we don’t know necessarily know what the endpoint looks like until runtime.

The HTML5 Database

So now to storing the results of our RESTful calls. The solution lies in another HTML5 feature – HTML database. In Safari and on the iPhone this means SQLite.

I’m a real fan of SQLite. Great small footprint relational database. I’ve been using it for some time now under .NET as storage for my Outlook plug in.

Storing Service Results

I chose a very simple schema for my database – simple Key/Values in a table using TEXT field types. SQLite, jQuery and HTML5 make it very easy for me:

function initDatabase()
{
  db = openDatabase('iRetailPassport', '1.0', 'iRetailPassport', 1000000);
 
  db.transaction(
    function(transaction){
      transaction.executeSql('CREATE TABLE IF NOT EXISTS settings (k TEXT NOT NULL PRIMARY KEY ON CONFLICT REPLACE, v TEXT NOT NULL );');
    }
  );
}

A couple of important points about the code above:

  • CREATE TABLE IF NOT EXISTS is very useful and part of the zero administration nature of SQLite. This statement means I can create and open a database in a single statement. And the transaction succeeds even if the database already exists.
  • CONFLICT REPLACE is also very useful. It allows me to do an insert even if the record exists. Also knows as an UPSERT or MERGE operation, this save time and code later on.

Because I’m making JSON service calls, the call results are both efficient and ready to store in my database:

$.ajax(
{
  type: "GET", url: "/iretailpassportsvc/clientservices.svc/passport",
  dataType: "json",
  cache: false,
  timeout: 1000,
 
  success: function(passport)
  {
    db.transaction(
      function(transaction){
        transaction.executeSql("INSERT INTO settings (k,v) VALUES ('passport',?);", [passport], null);
      }
    );
  },
 
  error: function()
  {
    setError(serviceUnavailableString);
  }
});

Notice the jQuery AJAX call to the server is marked as JSON. It is also important to set a timeout to ensure this function returns if the server is unavailable.

The AJAX method has both ‘success:’ and ‘error:’ callbacks. In the error callback, I’m handling the error. A likely implementation here is to start some sort of dialogue with the user when the event is fired and to cancel that dialogue on error. A dialogue could be information on a status or progress bar for example.

The success callback does the work of posting the JSON response to the database. Because I used ‘CONFLICT REPLACE’ in the database definition, the ‘INSERT’ will be a merge insert.

The success case would also update the user interface with fresh data but I have omitted this from the example to keep things simple.

Storing Images

In my application I was making a call to the REST service to get an image of a barcode that represents the users’ unique identifier. My original design was based on making this call as and when required and for the service to generate the image at run time and stream the image to the client. The strategy to generate at run time meant I could keep storage requirements low at the server and meant I didn’t have to worry about any referential integrity between the user identifier and the image. I wanted to keep this strategy for the offline application and reuse the service interface.

This meant storing an image streamed by the service. Well again, HTML5 comes to the rescue with the <canvas> element.

A canvas is a bit oriented drawing area – a bitmap.

The DOM supports a number of drawing operations to the canvas and also provides methods for serializing the canvas:

var src = "/iretailpassportsvc/clientservices.svc/barcode/" + passport;
var canvasImg = document.createElement("canvas");
var serializedVal = 'data:,';
var img = new Image();
 
img.onload = function() 
{
  canvasImg.width = img.width;
  canvasImg.height = img.height;
  canvasImg.getContext("2d").drawImage(img, 0, 0);
  serializedVal = canvasImg.toDataURL();
  $("#barcode").replaceWith(img);
 
  db.transaction(
    function(transaction){
      transaction.executeSql("INSERT INTO settings (k,v) VALUES ('barcode',?);", [serializedVal], function (transaction, results){setTimeout("jQT.goBack()", 1000)});
    }
  );
};
 
img.src = src;

This is very cool. In the code above I am creating <canvas> and <img> elements. I’m then loading the image using a reference to the image service endpoint with img.src = src.

During the image onload() event I set up the canvas dimensions to match that of the image (remember, the canvas is really a bitmap) and then draw the image to the canvas with the drawImage() statement.

The result of this is the canvas is now a bitmap representation of the image loaded from the server.

Finally, I serialize the image to a data URL. A data URL is a BASE64 representation of a resource and can be used anywhere a resource is used. So in this case it will be something like:

data:image/png;base64,……

So now I have a serialized string representation of the image generated at run time by my service. All I need to do now is save it to the database.

Later on, I can load this string  and use a simple statement like:

img.src = data

to display the image.

Asynchronous Everywhere

Remember that calls to the database and AJAX are asynchronous. Methods around these calls provide callbacks and you need to put your code inside the callback or you will see some very odd behaviour.

I got caught out when handling the image serialization. The call to load the image is delayed because the source is a network resource. So the img.onload() may take time to complete. My mistake was to write the serialized canvas data to the database in code following my img.src = URL. This had very strange results and was saving incomplete data (thank goodness for Web Inspector or I may still be sitting scratching my head).

The fix was to put all the serialization to database inside the img.onload().

Offline/Online Data Strategy

Now we know how to white list service calls, where to store the call results and how to store these results. One final point is around the offline/online data strategy.

A key benefit of a RESTful architecture is to provide fluid network applications without the need for holding vast datasets client side. So, if you need data or need to transition state, you call a service method. An offline model goes against this and necessitates storing data working with aged data.

But offline working cannot be ignored on mobile devices – just try using the internet on the train or underground.

The trick is to strike a balance between offline data fetches and online service fetches.

One strategy for offline/online data is to always make a service call and if the call fails or there is a timeout, read from the database. This helps keep the age of the data low but will result in a choppy user interface with a lot of ‘Please Wait’ timers.

A better strategy is to do things the other way round. That is, always read from the database. At the same time, issue a service call. If the call comes back with data, update the cache and user interface. To help with usability, you can always display a ‘Refreshing…’ message to the user is a status area.

Conclusion

As you can see from the this blog, the theory of offline RESTful applications is not a big challenge. The implementation raises a number of pitfalls which I hope I have been able to highlight.

But when it works, believe me…It’s all worth it.

No comments:

Post a Comment