Sencha Inc. | HTML5 Apps

Taking Sencha Touch Apps Offline

Published Apr 22, 2011 | James Pearce | Tutorial | Medium
Last Updated Aug 16, 2011

This Tutorial is most relevant to Sencha Touch, 1.x.

Mobile and tablet users are now first-class citizens of the web, and with Sencha Touch, we can create beautiful web-based applications for them. That is, until they drive into a tunnel. Or get on a plane. Or stray too far from cellular coverage.

Yes - unfortunately, offline usage of mobile devices is a fact of life. As application developers, we need to be sensitive to the fact that, unlike a desktop environment, the connectivity of the browser can vary wildly - and we need to ensure our applications cope elegantly with these changing conditions.

Native application enthusiasts have often pointed to this need for offline access as being one of the factors that prohibits the feasibility of web-based application development. In this article, we'll set out to prove this argument wrong. Through the magic of HTML5 and the JavaScript APIs available in contemporary mobile devices, we'll show how a simple Sencha Touch web application can still function in such disconnected situations.

Sencha Touch, meet Flight Mode...

Getting Set Up

To start off with, let's build ourselves a simple application. We can start with a Hello World application, similar to that which we've used in other tutorials. This is the app we will take offline. The entry point is an index.html file:

<!DOCTYPE html>
<html>
    <head>
        <title>Hello World</title>
 
        <script type="text/javascript" src="lib/touch/sencha-touch.js"></script>
        <link href="lib/touch/resources/css/sencha-touch.css" rel="stylesheet" type="text/css" />
 
        <script type="text/javascript" src="app/app.js"></script>
 
        <style>
            .hello {
                background:#ffffff;
                background-image:url(http://src.sencha.io/320/http://chuvachienes.com/wp-content/uploads/2009/07/hello.jpg);
            }
            .world {
                background:#ffffff;
                background-image:url(http://src.sencha.io/320/http://www.unc.edu/world/College_Updates_2009/world-globe.jpg);
                background-repeat:no-repeat;
            }
        </style>
 
    </head><body></body>
</html>

This contains the two familiar references to the Sencha Touch JavaScript and stylesheet files, an app/app.js file which will contain all of our application code, and then a little in-line styling to provide the app's graphics. Assuming we are running this out of a folder called offline, the project structure looks like this:

In app.js itself, we simply have two tabs with are given classes that will cause them to exhibit the styling from index.html above:

var helloWorld = new Ext.Application({
    launch: function() {
        this.tabs = new Ext.TabPanel({
            fullscreen: true,
            dockedItems: [{xtype:'toolbar', title:'Hello World'}],
            tabBar: {
                ui: 'light',
                layout: {
                    pack: 'center'
                }
            },
            items: [
                {cls:'hello', title:'Hello'},
                {cls:'world', title:'World'}
            ]
        });
    }
});

Nothing clever here. On the launch event we create a new TabPanel with a docked toolbar and the two tabs.

Run it up and you should see a familiar two-tabbed application with the resized images on each tab:

Now... let's get this working offline.

The App Cache Manifest

This application comprises a number of resources. In addition to the index.html file, we have our app code itself and the two Sencha Touch files. We need to indicate to the browser that it needs to aggressively cache these resources so that if we open the app in a browser on a subsequent, offline, occassion, all the files it needs are available locally.

To do this, we use something called a 'cache manifest'. This is a text file associated with the main index.html document and which lists all of the resources that the browser needs to cache in this way.

The manifest file itself is very simple. It is literally a list of the files, relative to the index.html location, with CACHE MANIFEST at the top:

CACHE MANIFEST
 
index.html
app/app.js
 
lib/touch/sencha-touch.js
lib/touch/resources/css/sencha-touch.css

Save this as app.manifest in the same directory as index.html.

To ensure that the browser uses this file to build its cache, we need to reference it in the top of the index.html file, using the manifest attribute of the html element:

<!DOCTYPE html>
<html manifest="app.manifest">
    <head>
        ...

There's one more trick, which is to ensue that your web server serves up the manifest file with the correct MIME type. It needs to be text/cache-manifest. The way you do this will depend on your web server. For nginx, for example, you'd need to add the following line to your types directive to make sure this is the type sent with files with the .manifest extension:

types {
    ...
    text/cache-manifest   manifest;
    ...
}

In Internet Information Server, you can use the administration UI to add new MIME types, and in Apache you could either add the following line to your mime.types file:

text/cache-manifest manifest

or the following to your .htaccess file for this area of the server:

AddType text/cache-manifest manifest

With those changes in place (and your server restarted if necessary), visit the application again with your device or a desktop browser. Using the latter might allow you to see the caching activity, as in Google Chrome, below:

Now, if you take your browser offline (which may be as simple as putting your mobile device into flight mode, disabling the network between your browser and the server, or even just stopping the server), you should be able to refresh the page and still have the app display:

(Notice how Google Chrome also provides console information about the fact it has fetched it from its cache.)

Of course, you'll notice that the main graphical element of the page is missing! Of course, we forgot to add those two external URLs to the manifest, and so they weren't cached as part of this process. They might have been cached by the browser anyway, so you might be lucky - but the point is that we didn't explicitly declare them into this manifest-led cache, and we probably should. We can add them now:

...
http://src.sencha.io/320/http://chuvachienes.com/wp-content/uploads/2009/07/hello.jpg
http://src.sencha.io/320/http://www.unc.edu/world/College_Updates_2009/world-globe.jpg

Now of course, the browser won't be able to fetch this file again - and then fetch and cache these images - if it remains offline. So reconnect everything and refresh the app to get the new manifest. If you disconnect again, they should now be cached and the app shows in all its glory, offline. (Notice the little orange plane on the screenshot from a real iPhone below - the app is working perfectly with the network disabled.)

The exact behaviour of the app cache, and what can be done with the manifest file, can be more interesting than this, and you're advised to read this article for more details.

Suffice to say, one frustration you may have using it is that you need to update the manifest file every time you change any of the referenced files. Otherwise the browser will never refetch them: if the 'parent' manifest file has not changed, it continues to use the cached copies. So it's highly advisable to remove the manifest attribute while you are developing the JavaScript app in this article - we'll put it on again at the end once the app is finished.

Once you've deployed an app with a manifest and you find you do need to force browsers to download new versions of the same files, you just have to make some trivial alteration to the manifest to trigger their refresh. You can use # characters to prefix comments in the file, so one idea is simply to place a commented version number in the file which you can change when parts of the app have:

...
# Hello World version 0.461
...

Let's move on to see what else we can do with mobile web apps in an offline world.

Storing Data Offline

In the previous section we looked at caching resources used to build the app. But many modern web apps subsequently load data from either the origin server (or another source) to provide the app's functionality. For example, you might have a list of recent photos from a gallery which are downloaded by the app once it's running.

Obviously we need to be online to pull this sort of data the first time, but wouldn't it be good if our app (which now runs offline!) could also see a cached version of this retrieved data?

We've now moved beyond the realm of the browser's resource caching mechanisms and into the application itself. We'll need to manage how this capability works in our own code. The good news is that Sencha Touch provides a set of data store and proxy classes that make it very easy to work with data from (and going to) a variety of sources - both server- and client-side.

For this example, let's add a new tab to our application which contains a gallery list. We will populate this list with the titles of a set of photographs. (We can get photographs matching certain keywords by running a YQL query against Flickr.)

The gallery list is easy to add in app.js:

items: [
    {cls:'hello', title:'Hello'},
    {cls:'world', title:'World'},
    {
        cls: 'gallery',
        title: 'Gallery',
        xtype: 'list',
        store: null,
        itemTpl:'{title}'
    },
]

You'll notice that we haven't declaratively bound this list to a store. But we have given it a title and a CSS class, and the template will simply display the photo's titles.

Also, in the launch function, let's create a reference to this list:

this.gallery = this.tabs.items.getAt(2);

Sencha Touch lists rely on being bound to stores of model instances. Let's define the Photo model in the application, and give it two fields, ID and title:

Photo: Ext.regModel('Photo', {
    fields:[
        {name:'id'},
        {name:'title'},
    ]
}),

Then we should also create a store for these photos, and hook it up to the YQL server to fetch the most recent 10 photos matching a keyword. We use JSONP (i.e. the ScriptTagProxy) as the data comes from another origin server:

onlineStore: new Ext.data.Store({
    model: 'Photo',
    proxy: {
        type: 'scripttag',
        url: 'http://query.yahooapis.com/v1/public/yql?format=json&q=' +
            escape(
                'select * from flickr.photos.search where text="hello" limit 10'
            ),
        reader: new Ext.data.JsonReader({
            root: 'query.results.photo'
        })
    }
}),

The dotted root property of the reader allows us to point three levels down inside the returned data structure to reach the 10 photo records.

Now, remember we have neither bound this store to the list control, nor in fact set the store to autoLoad (for reasons which will become apparent soon). Let us explicitly load the store with the external data at end of the application's launch event, having used an event listener to bind it to the list when the loading is complete:

this.onlineStore.addListener('load', function () {
    helloWorld.gallery.bindStore(helloWorld.onlineStore);
});
this.onlineStore.load();

When we refresh the app, our third tab appears. What's more, it lists the titles of the last 10 photos related to the keyword 'hello', just as we'd hoped:

Now if you're still using the cache manifest at this point (but have been updating its version as you've edited app.js!), you'll notice that the list is blank. Our manifest as it currently stands indicates to the browser that it should get absolutely everything from its offline cache - and so of course the YQL service isn't even contacted. We need to add the following section to allow the browser to try to go online for resources other than our explicit file list:

NETWORK:
*

Add this, refresh the app (possibly twice, to refresh all the app files too), and you'll notice that the browser will now contact Yahoo, even if the app files themselves are coming from the local cache.

If we're using the cache manifest, and do actually take the device offline, the app should still run of course. But now, of course, despite our new NETWORK wildcard, the service can't be reached. Unsurprisingly, the list is blank in flight mode:

To solve this, we could have squirreled away the data when we were online. At least when we went offline, the app would be able to display this local copy instead. Of course it would just be a snapshot, but still better than nothing.

One effective way to do this is to create a second store in the application, and use a LocalStorageProxy to persist its contents to the device's local storage. When the 'online' store loads successfully, we can fill this 'offline' store up with fresh data. But when it can't, such as when we are in flight mode, the 'offline' store snapshot can be used.

Firstly, let's create this second store:

offlineStore: new Ext.data.Store({
    model: 'Photo',
    proxy: {
        type: 'localstorage',
        id: 'helloworld'
    }
}),

Notice how this is much simpler than the first store: this doesn't need to fetch its own data, and merely needs the model type and a unique id that is used for the keys in the local storage database.

One trick to making this work is being able to detect that the 'online' store load has failed quickly, and then switch to the cached 'offline' store. We can do that by setting an aggressive timeout on the JSONP proxy, and registering a listener for the resulting exception if nothing loads:

timeout: 2000,
listeners: {
    exception:function () {
        console.log("I think we are offline");
        helloWorld.gallery.bindStore(helloWorld.offlineStore);
        helloWorld.offlineStore.load();
    }
}

In the exception handler, we've bound the offlineStore to the list, and loaded it. But, as yet, this is going to be empty! We also need to ensure that a successful onlineStore load will flush it and re-fill it.

We can do this easily by beefing up the load event during the launch:

this.onlineStore.addListener('load', function () {
    console.log("I think we are online");
    helloWorld.offlineStore.proxy.clear();
    this.each(function (record) {
        var photo = helloWorld.offlineStore.add(record.data)[0];
    });
    helloWorld.offlineStore.sync();
    helloWorld.gallery.bindStore(helloWorld.offlineStore);
});

Here we clear out the offline store, and then, for each record in the onlineStore, we create a new model instance in the offlineStore. We synchronize that store to save it into the localStorage, and then bind it to the list. (We may as well bind the offline store to the list whether we are online or offline.)

Now it may seem slighly odd that we are explicitly binding the store to the list now that it's always offlineStore that gets that honor. The reason is simply that we avoid the list trying to keep updated with all our bulk purging and refilling by binding it explicitly once we know the records are all ready to go.

Taking the device online once more, update the manifest, refresh (to get the online data and stash it away), and then go into flight mode again. The most recent set of data is still there:

Mission accomplished!

Storing Images Offline

Now wait a minute, you might be saying. I thought these were photos, but we're only seeing the titles! Well yes. Of course we can use the data that comes back from the YQL request to construct the URL of an image, and we can display it. Firstly, add some extra fields to the model:

fields:[
    {name:'id'},
    {name:'farm'},
    {name:'owner'},
    {name:'secret'},
    {name:'server'},
    {name:'title'},
    {name:'url'},
]

The first five of these are concatenated together to create the final URL field like this:

setUrl: function() {
    var url = "http://farm" + this.get('farm') +
        ".static.flickr.com/" + this.get('server') +
        "/" + this.get('id') + "_" + this.get('secret') + "_s.jpg";
    this.set('url', url);
}

We add this function to the model, and then make sure call it just after the model instance is created in our load handler:

this.each(function (record) {
    var photo = helloWorld.offlineStore.add(record.data)[0];
    photo.setUrl();
});

With this URL present in each model instance, we can change the item template for the list to display a thumbnail with it:

itemTpl:'<img src="{url}" />{title}'

That works, but let's make it look a little nicer with some extra CSS in index.html. Simple borders, shadows and so on:

.gallery img {
    border:1px solid #fff;
    margin-right:8px;
    vertical-align:middle;
    -webkit-box-shadow: 0 2px 4px #777;
    float:left;
}
.gallery .x-list-item {
    background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ddd), to(#d7d7d7));
    text-shadow: 0 1px 1px #fff;
    font-size:0.8em;
}

And, assuming your cached resources are all refreshed and you are back online again, you should see something like:

Now, if you take this offline again and refresh, the chances are that many of the images will still show, since Yahoo sets a very long expiry time on these images and your browser will probably have cached them for performance reasons based on the HTTP headers alone.

But we can't always be sure that this 'implicit' caching will be honored by the browser, nor that the images we are being sent will not expire more aggressively. In the general case we need to figure out how we can explicitly cache the image content locally, and under the control of our own application.

This is not as easy as it sounds, unfortunately. JavaScript does not have read/write access to the binary content of the images in a web document, and so we can't read the image when it is there, store it ourselves, and then write it back out again when offline.

And it might be fiddly to use the manifest to indicate that we want them stored offline if we don't know the URLs ahead of time. What would be really cool would be to work out how to be able to get an encoded textual representation of the image and then store it in our offlineStore with all the other model data.

One key to this is the ability use 'Data URLs'. These are encoded strings used in place of regular URLs which allow you to embed media, such as images, inline to markup or styling. For example, you can load an image into a page like this:

<img src='data:image/png;base64,iVBORw0KGgoAAAA...' />

The main part of the string is a base64-encoded serialization of the image.

But the great thing about this is that we could easily keep these strings stored in our offlineStore (replacing the original online URL), and reconstruct the images using the data URL.

The question then becomes: how do we construct these strings from the original images? A common technique for doing this is to create a <canvas> element in the DOM, paint a previously-downloaded image on to it, and then call the element's toDataURL() method to receive the string we need. Something like:

function imgToDataUrl(img) {
    // where 'img' is a reference to a loaded image element
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    canvas.getContext("2d").drawImage(img, 0, 0);
    return canvas.toDataURL("image/png");
}

However, there is one huge issue with this approach, as alluded to in the canvas specification, namely that this only works if the image has been downloaded from the same origin as the page or application. In our case, we're pulling in images from Flickr, so attempts to use the canvas technique above will fail, throwing security exceptions.

As with any cross-domain-related issue, one solution is to use a proxy on your own server to channel the images through. That's perfectly adequate for this purpose. Nevertheless it's a shame to have to use server bandwidth simply to overcome this issue, and it doesn't quite fit with the 'thick client, thin server' architecture we're going for here.

What needn't be done on your own server (and what can't be done on the client) can often be done in the cloud. In this case, the sencha.io service can help. As well as resizing images to fit mobile screens, the service can encode them to the data URL format and make them available to your client application over JSONP.

Using this, we can embellish the setUrl() function on our model, and use it to replace the 'real' URL with the data URL equivalent. Using JSONP is, of course, an asynchronous exercise, so we'll need to create a callback that can re-access the model instance and update the URL field. sencha.io allows us to pass extra arguments that will be placed in the callback, so we can use the ID of the model to be sure we're updating the right one at callback-time.

As usual, a JSONP request is made by adding in extra script elements to the document. Sadly we can't use the regular Sencha Touch JSONP/ScriptTagProxy mechanisms, since sencha.io's API scheme doesnt match the same querystring convention. But it is still very simple:

var script = document.createElement("script");
script.setAttribute("src",
    "http://src.sencha.io/data.helloWorld.setPhotoUrl-" + this.getId() +
    "/" + url
);
script.setAttribute("type","text/javascript");
document.body.appendChild(script);

And, from the resulting script source URL that looks like:

http://src.sencha.io/data.helloWorld.setPhotoUrl-123/http://mysite.com/myimage.png

...sencha.io will return a fragment of JavaScript that looks like:

helloWorld.setPhotoUrl('123','data:image/png;base64,iVBORw0KGgoAAAA...');

We need to implement that setPhotoUrl callback function (in the helloWorld application) to set the model instance's URL to be that data URL string:

setPhotoUrl: function (id, dataUrl) {
    var photo = this.offlineStore.getById(id);
    photo.set('url', dataUrl);
    this.offlineStore.sync();
},

Here, we simply fetch the photo from the offlineStore, set the URL, and sync it back to the localStorage. You can remove the synchronous photo.set('url'...) line from the setUrl function now, too.

Refresh the application. Even when it's online, you should now be able to see (using a DOM inspector of some sort) that the image URLs have been rewritten to be the data URL format:

Yes, pulling these in from sencha.io as base64 is less efficient, bandwidth-wise, than getting the original things. But if you are able to take a look at the localStorage records, you will see that these data URLs are now stored there too. This is what we've been trying to achieve all along.

This means that when we take the application offline...

...the images are still preserved. Mission accomplished, take two, and our app.js file is complete:

var helloWorld = new Ext.Application({
 
    Photo: Ext.regModel('Photo', {
        fields:[
            {name:'id'},
            {name:'farm'},
            {name:'owner'},
            {name:'secret'},
            {name:'server'},
            {name:'title'},
            {name:'url'},
        ],
        setUrl: function() {
            var url = "http://farm" + this.get('farm') +
                ".static.flickr.com/" + this.get('server') +
                "/" + this.get('id') + "_" + this.get('secret') + "_s.jpg";
 
            var script = document.createElement("script");
            script.setAttribute("src",
                "http://src.sencha.io/data.helloWorld.setPhotoUrl-" + this.getId() +
                "/" + url
            );
            script.setAttribute("type","text/javascript");
            document.body.appendChild(script);
        }
    }),
 
    setPhotoUrl: function (id, dataUrl) {
        var photo = this.offlineStore.getById(id);
        photo.set('url', dataUrl);
        this.offlineStore.sync();
    },
 
    onlineStore: new Ext.data.Store({
        model: 'Photo',
        proxy: {
            type: 'scripttag',
            url: 'http://query.yahooapis.com/v1/public/yql?format=json&q=' +
                escape(
                    'select * from flickr.photos.search where text="hello" limit 10'
                ),
            reader: new Ext.data.JsonReader({
                root: 'query.results.photo'
            }),
            timeout: 2000,
            listeners: {
                exception:function () {
                    console.log("I think we are offline");
                    helloWorld.gallery.bindStore(helloWorld.offlineStore);
                    helloWorld.offlineStore.load();
                }
            }
        }
    }),
 
    offlineStore: new Ext.data.Store({
        model: 'Photo',
        proxy: {
            type: 'localstorage',
            id: 'helloworld'
        }
    }),
 
    launch: function() {
        this.tabs = new Ext.TabPanel({
            fullscreen: true,
            dockedItems: [{xtype:'toolbar', title:'Hello World'}],
            tabBar: {
                ui: 'light',
                layout: {
                    pack: 'center'
                }
            },
            items: [
                {cls:'hello', title:'Hello'},
                {cls:'world', title:'World'},
                {
                    cls: 'gallery',
                    title: 'Gallery',
                    xtype: 'list',
                    store: null,
                    itemTpl:'<img src="{url}" />{title}'
                }
            ]
        });
        this.gallery = this.tabs.items.getAt(2);
 
        this.onlineStore.addListener('load', function () {
            console.log("I think we are online");
            helloWorld.offlineStore.proxy.clear();
            this.each(function (record) {
                var photo = helloWorld.offlineStore.add(record.data)[0];
                photo.setUrl();
            });
            helloWorld.offlineStore.sync();
            helloWorld.gallery.bindStore(helloWorld.offlineStore);
        });
        this.onlineStore.load();
    }
 
});

Connectivity Detection

We've introduced the basic principles of taking an application and its data offline, using cache manifests and localStorage.

One thing we have not explicitly dealt with here is the scenario of 'fuzzyline' - when a device is arbitrarily on and offline during the usage of the application. In other words, where you can't say whether any particular request to a server or service will be successful or not.

The way in which you deal with this sort of environment is likely to be very dependent upon what your application is trying to achieve, but you probably need to err on the side of defensiveness. If you are pulling in external data, you might try building retry algorithms into your store's loading sequence for example. A failed or timed-out request causes the application to wait some time before trying again.

In some cases, you have the opportunity to query the browser's opinion about whether it is on- or offline. The navigator.onLine property returns a boolean flag, so you could easily add an extra tab thus:

{
    cls: 'status',
    title: '?',
    html: navigator.onLine ? 'online' : 'offline'
}

When the application is started up, this will articulate the state:

(You can also use the online and offline events to detect when the state changes.)

Note however, this this property seems be more reliable at detecting whether the browser been put explicitly into flight or offline mode than whether it actually has a suitably good connection to make a successful requests. (This is why, in the main example above, we relied on a timeout on the request to determine offlinedness). Google Chrome on a Mac, for example, believes it is online, even if the computer's network has been disabled - as does the iOS simulator.

Hopefully we can expect browser support for this property and events to improve and become more consistent.

If you want to take a more active approach to detecting your device's connectivity, and if you are using PhoneGap to wrap and distribute your web apps, you might want to take a look at that library's Network API. This allows you to test whether a given hostname is reachable, and it also gives you some feedback about what sort of connection it is (e.g. WiFi or cellular) which might also affect how you want your application to react.

The Kitchen Sync

You'll have noticed that we have quite deliberately worked through examples above where the data was being pulled from a server, and was read-only. For many types of application, taking a snapshot of recent data offline is quite reasonable.

But the Sencha Touch stores and proxies are also designed to allow applications to write data and send it back to a server. Think of a REST-ful API through which records can be fetched to the device. Their details can be examined, changes made - or perhaps new records can be created and old ones deleted. These changes can then be updated back to the server through the same API. (Take a look at the RestProxy documentation for examples.)

If the device is online, this process is relatively straightforward - the proxy can sync the data back to the server immediately and there is little danger that the server dataset and client dataset will diverge.

If the device is offline, we have shown that the user can be presented with a snapshot of the data from when they last connected. This at least makes it possible for them to review those records when offline: reading semi-fresh email on a plane, for example.

But many applications will involve the user adding, changing, or creating records. Should this functionality be disabled when the device is offline. That's certainly one option, but there's a good chance that a user would prefer to work with the data when offline, knowing that these changes will propogate back to the server when they reconnect. Allowing a user to read email offline but not compose replies, for example, seems draconian.

This may add significant extra complexity to your application, since you are now responsible for keeping track of the changes made to the records in the offline state, and then ensuring that those get applied later. To a certain extent, the existing store and proxy sync() technique eases this process on the client, but server-side, you will need to think carefully about reconciliation. If an offline user has made changes to a record on their client, and an online user has made changes to the same record (syncing immediately to the server), your server application is now responsible for merging those changes together, dealing with any likely conflicts, and ensuring those changes are sent back to each user.

As well as batching all the changes, you might need to start timestamping updates, so they can be applied on the definitive server store in the right order. If multiple users are offline, this could get even trickier of course. You might choose to use a queue pattern (like an 'outbox' for those offline emails) which the clients can flush at their earliest opportunity.

It may seem as though we are finishing this article with a list of challenging hurdles, and there is no doubt that multi-directional synchronization betwen occasionally-connected devices can be a daunting subject. But synchronization is a computer science problem that has been solved in other arenas - disk mirroring for just one simple example - and it is to be expected that stable and common patterns will emerge for in the web realm too. Sencha will continue to endeavor to make it as easy as possible for you to create such applications in a modern, mobile web app world.

Summary

In this tutorial, we have shown how you can use a cache manifest, attached to the entry point of your application, to explicitly declare the resources that comprise your app, so that it can be used once the user loses their connection. Although it's fiddly to develop against (and, no doubt, by now you are an expert at clearing browser caches...), it's a reliable way to let your web app go where the radio waves can't.

We then took a look at how you can use multiple stores in a Sencha Touch app to have cached (and read-only) offline records that are refreshed from an online source when it's available. We then looked at how you can serialize binary objects, such as images, using data URLs - and then cache them into the same offline store. Using a server- or cloud-side service, we showed how this can even work for different-origin resources.

We concluded by discussing other ways in which you can detect a device's state of connectivity and adapt your applications behavior, before concluding with a glimpse at some of the challenges inherent in allowing offline apps to have read/write access to records which will subsequently need to be synchronized with a server.

The web is no longer one of documents, of naive requests and responses, and of ever-connected thin clients and thick servers. HTML5 and its related technologies are helping us to explore a world in which the web is one of applications, of efficient data transfer and synchronization, and of partially connected thick clients and thin servers - not to mention a helpful cloud.

These technologies are helping us to build web applications which are increasingly indistinguishable from their native brethren. Hopefully this article has helped you to explore some of their possibilities.

Share this post:
Leave a reply

Written by James Pearce
James Pearce heads developer relations at Sencha. He is a technologist, writer, developer and practitioner, who has been working with the mobile web for over a decade. Previously he was the CTO at dotMobi and has a background in mobile startups, telecoms infrastructure and management consultancy. James is the creator of tinySrc, the WordPress Mobile Pack, WhitherApps, modernizr-server and confess.js, and has written books on mobile web development for both Wiley and Wrox.
Follow James on Twitter

9 Comments

Jake Cohen

3 years ago

I recently applied this to an application I am working on.  It cached fine on the browser when I was testing, but did not work offline on my iPad. The problem was that my cache manifest file was called “app.manifest” when iOS requires any manifest file to be called “chace.manifest”

Hope this helps!

Terry Wilkinson

3 years ago

I’m new to Sencha Touch and thanks for the detailed example.  I do have one question about the section on ‘Storing Data offline’:

If I understand correctly, your example results in 2 copies of all of the data in the application being stored on the mobile device, one in the onlinestore and the other in the offline store.  Maybe I’m missing something fundamental, but wouldn’t this be a problem when dealing with large amounts of data?

Thanks,

Dmitry Kharlamov

3 years ago

Thanks for the great examlpe and a brilliant article!

@Terry:
I am fairly new, too. However, my understanding is that JsonP proxy of the onlineStore used to load the YQL is a Server proxy and stores data inside a linked JS file — a response from the server. That data is stored in browser cache. Whereas offlineStore uses a Client proxy and stores the data in localStorage. It’s browser’s responsibility to manage the RAM it uses for cache thus Server proxies don’t have clear() method. There is nothing you can do about having data in two places in this particular example because you need to load it somehow and store it somehow, so removing either of the stores from the equation or not synchronising the offlineStore will break the functionality. I hope that clarified things.

Dmitry

Max

3 years ago

Thanks for the great example!

Two coding comments: First, for the Flickr image url concatenation you might try using the convert function on the “url” field rather than coding a setUrl() method. Convert is described in the Ext.data.Field documentation. Secondly, I’m curious about the reason you couldn’t use a script tag proxy. By the way, I tried your example using Ext.util.JSONP.request(), and except for a small glitch in the current implementation, it worked fine.

James Pearce Sencha Employee

3 years ago

@Max, if that implementation stayed as shown earlier in the article, indeed, the convert function would be perfect. But when we need to take the images offline, I chose to continue to use that function to handle the base64 stuff. Yes, I think you could use Ext.util.JSONP.request - what I think I meant was that the standard Ext.data.proxy implementations are not suited to the URL syntax that src.sencha.io requires.

Mukul Seth

3 years ago

Hi James,

What are the limitations on the offline stores as shown in this example? I see that Sencha Touch uses HTML5 localStorage to achieve this goal.

It appears that most browsers limit localStorage to 5 megabytes. What should be expected if the offline stores exceeds that capacity?

I think if an application is caching a lot of images, textual data that limit can be hit very easily.

Is it possible to cache the offline stores using an HTML5 database instead (which can have a larger limit) still leveraging the Sencha stores without having to build that functionality from scratch. Please forgive my ignorance if I’m not making any sense at all ... raspberry

hao

3 years ago

Hi James,

Thanks for the nice post! But if I’d like to wrap this app with PhoneGap and deploy to iPhone, how do i set the correct MIME type to the manifest file?

Thanks

Johan

3 years ago

Awesome article!

Would be great if you also could briefly discuss to what extent offline functionalities could be used in order to speed up the loading of apps? I’m assuming that when you are in production/live you would simply put all code as offline and never refresh it…? Is that the standard way of doing it?

/Johan

Gustavo Adrian

3 years ago

One well known way to deal with synchronization of changes made in the app after being offline is using the optimistic locking pattern, used to handle concurrency.

Adding a “version” field to each entity (being an integer preferred, not a datetime or similar types, which could be a problem if there’s a lot of concurrent users. Two or more records could end with the same datetime), and checking against this value before update or delete a record. If the value of the version field of the offline record differs from the online version, the action should be cancelled.

Used in conjuntion with transactions should be enough to handle these cases.

Leave a comment:

Commenting is not available in this channel entry.