Keeping common source between Phonegap and HTML5 apps
I have written a small utility app to help compose SMS texts and emails out of text snippets – TextByNumbers. Initially I created this as an HTML5 app using jQueryMobile and Knockout.js. It seems to work OK in its HTML5 form however I wanted to explore how easy it was to get into natively hosted apps using Phonegap. The app is completely client side – the server is only used to serve files - so it should be easy to port. There were some advantages I was looking for by being native
- HTML5 apps depend upon the support offered by the browser which can be patchy
- HTML5 offline apps can be “uninstalled” if the browser cache is flushed
- HTML5 offers no real method of reading or writing to local storage, as I have not implemented a snippet editor within the app (text needs to edited externally this means that snippets need to be edited, uploaded to the server and then cached which is cumbersome.
In contrast Phonegap apps
- Offer uniform support, for instance Samsung phones running Android 2 do not implements the sms:// protocol properly however from phonegap this protocol does work.
- Are not reliant on a shared cache for its program assets (HTML, JS etc)
- Phonegap enables snippets to be edited externally (either on the phone or on a PC) and then stored on the local SD card. TextByNumbers then reads the snippets from local storage
Phonegap is a half-way house between HTML5 Applications and full native applications. It wraps the HTML / CSS / JavaScript assets in a chromeless browser. There is an apk file for Android phones if you want to have a go (at your own risk).
I was interested to find out how painless this process was and also how much code could be shared. It turns out that pretty much all of the code could be shared in fact I did not want to maintain separate assets for each version so I worked out the minimal set of changes needed to keep common source files.
Appcache reference
I needed to have an appache reference for the HTML5 app running in a browser, however this would be not used by phonegap. Luckily I just always include the reference and phonegap does not trip up. I only actually include the appcache file on the HTML5 app web site not in the native application. Like so
<!DOCTYPE html> <html manifest="txt.appcache"> <head>
Working out how the app is being run
I already had an Environment object so I extended it to have methods for determining which environment I was running in. There are two modes of operation for the app
- HTML5 app
Running in a browser.
URLs are HTTP:// based
Where everything including the snippets are served from the appcache - Phonegap app
Running as a native mobile application.
An embedded browser with FILE:// based URLs
Where the snippets are loaded from local storage
self.isPhoneBrowser = function() { return navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/); }; self.isNetworkUrl = function() { return !(document.URL.indexOf( 'http://' ) === -1 && document.URL.indexOf( 'https://' ) === -1); } self.shouldUsePhoneGap = function() { // only use phonegap / cordova if its a file URL in a mobile browser return !self.isNetworkUrl() && self.isPhoneBrowser(); }
I also added a method that would effectively check if the cordova (phonegap) JavaScript had been loaded and a knockout bound property appVersion (which is the code version displayed on the footer of each page) that is suffixed with the mode we are running in, “p” for phonegap and “b” for browser.
self.isPhoneGap = ko.computed(function() { return window.cordova != null; }, this); self.appVersion = ko.computed(function() { return '341' + (self.isPhoneGap() ? '(p)' : '(b)'); }, this);
Including Phonegap / Cordova JavaScript
I did not want to include any phonegap JavaScript if the app was being run in a browser so I went old school and added the script reference using document.write like this
<script src="viewmodel/environment.js" type="text/javascript"></script> <script language="javascript" type="text/javascript"> if ((new Environment()).shouldUsePhoneGap()) { document.write("<script type='text/javascript' charset='utf-8' src='cordova-2.7.0.js'><\/script>"); } </script>
Also there is some general advice on using phonegap and jQueryMobile that I included
<!-- Since jQuery Mobile relies on jQuery core's $.ajax() functionality, $.support.cors & $.mobile.allowCrossDomainPages must be set to true to tell $.ajax to load cross-domain pages. --> <script type="text/javascript"> if ((new Environment()).shouldUsePhoneGap()) { $(document).bind("mobileinit", function () { $.support.cors = true; $.mobile.allowCrossDomainPages = true; }); }; </script>
Hooking deviceready
There is a danger in attempting to use cordova / phonegap functionality before the JavaScript has initialised. For many pages this is not an issue as they are only using HTML5, jQueryMobile and Knockout assets and resources. However the snippets page needs to read the snippets from local storage if we are running as a phonegap app and that requires phonegap functionality. The deviceready event is the mechanism that should be used.
self.onDeviceReady = function () { // Now safe to use the PhoneGap API console.log("Device Ready..."); self.catagoryFile = new TextFileIO("tbn","catagory.txt"); self.snippetsFile = new TextFileIO("tbn","snippets.txt"); self.catagoryFile.readJSON(self.loadCatagories); }; if (self.environment.shouldUsePhoneGap()) { document.addEventListener("deviceready", self.onDeviceReady, false); } $( '#home' ).live( 'pageinit',function(event){ // need to do this here as we do not store the snippets in persistant storage // only do it on the page init as the page show event fires when backing out of the list view if (!self.environment.isPhoneGap()) { self.catagoryDataSource.getData(false, self.loadCatagories); } });
We use the TextFileIO objects, categoryFile and snippetsFile if we are a phonegap app and after the deviceready event has fired. If we are an HTML5 app then we just hang off the usual jQueryMobile event pageinit and load the data from an ajax call using the categoryDataSource object.
Unit Tests
It turns out that generated new phonegap app has support for a jasmine unit test runner and so I also included the unit tests in both versions.
The source files
I started from getting all the assets together that are needed to work with phonegap these are stored in <repo_root>\phonegap\android\assets\www and look like this
These files are built into the apk (or other deployable native app)
To release the HTML5 version to the web server I just copy www folder to my host and add in the following extra files
Then I can replace the categories and snippets in their text files in the data folder with a set than match my needs.