Tutorial with Examples - Converting a Simple Web Application to Cordova Mobile App

Written by NickWal

This is the third part of a tutorial demonstrating the ability to re-use JavaScript in many different environments. The first part dealt with the creation of the re-usable service, a simple command line wrapper which called the service as well as a simple test which checks the service is performing as expected. The second part of the tutorial discussed the development of a simple web application based on that service, using browserify to wrap the service so that it will run in any modern browser. This third part will take the web app, and show you how to use Apache Cordova, a platform independent JavaScript mobile phone development framework, to make your simple app run on modern smartphones.

This third part of the tutorial assumes that you have understood the concepts so far raised in earlier parts of the tutorial.

Project Set-up

You can continue to use the old directory containing the files from the previous tutorial, but I recommend you create yourself an new project environment like this:

Create a new directory mkdir crprice or md crprice or whatever your operating system supports and cd to it. Then use the following:

   npm install -g npm-package-bin xo ava gulp-cli
   npm init -y
   xo --init --esnext
   ava --init
   npm install --save angular bluebird request request-promise woofwoof 
   npm install --save-dev ava browser-sync browserify browserify-css fs-extra glob grunt grunt-browserify grunt-contrib-connect grunt-contrib-watch gulp gulp-rename gulp-util imagemagick vinyl-source-stream xo 

   npm-bin add crprice ./price-tool.js -n

Price Service

The price service is exactly the same as in the previous examples. Interestingly although the environment will be different - instead of running in a nodejs server environment, the application will be running in the sandboxed environment of the mobile phone, nevertheless we can still use exactly the same code, even to access network resources, by using the tool browserify. This not only resolves code that has been required through npm modules, but also provides a wrapper which allows the developer to access services that nodejs would provide natively through the browser instead.

Below the full source code of the service. Just copy and paste this and save with a name price-service.js

   /* jshint node:true */
   /* jshint esversion:6 */
   'use strict';
   const rp = require('request-promise');

   // https://coinmarketcap.com/api/

   module.exports = function () {
       this.getPrice = function (id) {
           const uri = 'https://api.coinmarketcap.com/v1/ticker/' + id + '/';
           const options = {
               url: uri,
               json: true
           };
           return rp.get(options).then(a => {
               return a;
           });
       };
   };

Web Application built with Gulp

For the web application, nothing changes from the previous tutorial. Its light web application using AngularJS which provides a neat framework allowing the developer to write minimal amounts of code. A simple gulp file is used to help in the developer cycle, watching the source code for changes, and rebuilding the target files as necessary and then firing off a reload event to the browser.

Please refer to the previous tutorial for a more in depth explanation. The source code is included here for your convenience.

   <html>
   <head>
     <title></title>
    <script src="scripts/app.js"></script>
    </head>
   <body ng-app="myApp">
   <div ng-controller="MyAppCtrl">
     {{variable}}
     <button ng-click="updateprice()">Update Price</button>
   </div>
   </body>
   </html>

   /* jshint node:true */
   /* jshint esversion:6 */

   'use strict';

   var angular = require('angular');

   angular.module('myApp', [])
   .controller('MyAppCtrl', ['$scope', 'priceService', function ($scope, priceService) {
       $scope.variable = '-none-';
       $scope.updateprice = function () {
           console.log('requesting price');
           priceService.getPrice('bitcoin').then(result => {
               var price = result[0];
               $scope.variable = price.symbol + ' ' + price.price_usd + ' USD';
               $scope.$apply(); // make sure async update is visible
           });
       };
   }]).service('priceService', require('../../price-service.js'));

   var path = require('path');
   var gulp = require('gulp');
   var browserSync = require('browser-sync').create();
   var gutil = require('gulp-util');
   var browserify = require('browserify');
   var sourceStream = require('vinyl-source-stream');
   var fse = require('fs-extra');
   var rename = require('gulp-rename');

   // process JS files and return the stream.
   gulp.task('js', function () {
       var bundleStream = browserify()
           .add('app/js/app.js')
           .ignore('cls-bluebird')
           .transform('browserify-css', {
       autoInject: true,
       rootDir: 'www',
       processRelativeUrl: function (relativeUrl) {
           var stripQueryStringAndHashFromPath = function (url) {
               return url.split('?')[0].split('#')[0];
           };
           var replaceAll = function (find, replace, str) {
               var found = find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
               return str.replace(new RegExp(found, 'g'), replace);
           };
           console.log(relativeUrl);
           var rootDir = path.resolve(process.cwd(), 'www');
           var relativePath = stripQueryStringAndHashFromPath(relativeUrl);
           console.log(relativePath);
           var queryStringAndHash = relativeUrl.substring(relativePath.length);

                   //
                   // Copying files from '../node_modules/bootstrap/' to 'dist/vendor/bootstrap/'
                   //
           var sep = path.sep;
           var prefix = '..' + sep + 'node_modules' + sep;
           if (relativePath.startsWith(prefix)) {
               var vendorPath = 'vendor' + sep + relativePath.substring(prefix.length);
               var source = path.join(rootDir, relativePath);
               var target = path.join(rootDir, vendorPath);

               gutil.log('Copying file from ' + JSON.stringify(source) + ' to ' + JSON.stringify(target));
               fse.copySync(source, target);

                       // Returns a new path string with original query string and hash fragments
               var result = replaceAll('\\', '/', (vendorPath + queryStringAndHash));
               console.log(result);
               return result;
           }

           return relativeUrl;
       }

   })
           .bundle();

       return bundleStream
           .pipe(sourceStream('app.js'))
           .pipe(gulp.dest('www/scripts'));
   });

   gulp.task('copyview', function () {
       return gulp.src('app/index.html')
           .pipe(rename({
       dirname: ''
   }))
           .pipe(gulp.dest('www'));
   });

   // Static server
   gulp.task('dev', ['js', 'copyview'], function () {
       browserSync.init({
           server: {
               baseDir: 'www/'
           }
       });
       gulp.watch('app/**/*.*', ['js-watch']);
   });

   gulp.task('js-watch', ['js', 'copyview'], function () {
       browserSync.reload();
   });
   gulp.task('vs', ['js', 'copyview']);
   gulp.task('default', ['dev']);

remember to use gulp vs to build the web app files. You’ll need to do this at least once before moving on to the next part of the tutorial

Converting this all to a mobile phone app

Our mobile phone app will be build using cordova. In addition to installing the Android SDK (or SDK for other mobile phone target platforms), you will need to install cordova itself.

        npm install -g cordova

Converting this all to a mobile phone application is surprisingly simple. Firstly you need to include in the project a configuration file for Apache Cordova. The example I’m using is here below.

   <?xml version='1.0' encoding='utf-8'?>
   <widget id="net.log1.apps.priceApp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
       <name>PriceTools-App</name>
       <description>
           A simple multiplatform mobile application that allows you to view the price of some well known crytpocurrencies in real time
       </description>
       <author email="[email protected]" href="https://www.infohit.net">
           Nicholas Waltham
       </author>
       <content src="index.html" />
       <plugin name="cordova-plugin-whitelist" spec="1" />
       <access origin="*" />
       <allow-intent href="http://*/*" />
       <allow-intent href="https://*/*" />
       <allow-intent href="tel:*" />
       <allow-intent href="sms:*" />
       <allow-intent href="mailto:*" />
       <allow-intent href="geo:*" />
       <platform name="android">
           <allow-intent href="market:*" />
       </platform>
       <platform name="ios">
           <allow-intent href="itms:*" />
           <allow-intent href="itms-apps:*" />
       </platform>
       <engine name="android" spec="~6.0.0" />
       <hook type="before_build" src="hook.js" />
   </widget>

There’s one small difference between this file, and most configuration files that you’ll find for apache cordova, and that’s the inclusion of a JavaScript hook which will run the gulpfile which runs the browserify tool, before the build step for Cordova. This is for convenience, making it much easier to develop continuous iterations of the app.

Here’s the source code for the hook.

   #!/usr/bin/env node

   var gutil = require('gulp-util');
   var chalk = require('chalk');
   var prettyTime = require('pretty-hrtime');


   module.exports = function (context) {
       var Q = context.requireCordovaModule('q');
       var deferral = new Q.defer();
       var taskCount = 0;

       var path = require('path'),
           gulp = require('gulp'),
           gulpfile = path.join(__dirname, 'gulpfile');

       require(gulpfile);
       var gulpInst = gulp;

       gulpInst.on('err', function () {
           failed = true;
       });

       gulpInst.on('task_start', function (e) {
           gutil.log('Starting', '\'' + chalk.cyan(e.task) + '\'...');
           taskCount = taskCount + 1;
       });

       gulpInst.on('task_stop', function (e) {
           var time = prettyTime(e.hrDuration);
           gutil.log(
               'Finished', '\'' + chalk.cyan(e.task) + '\'',
               'after', chalk.magenta(time)
           );
           taskCount = taskCount - 1;
           if (taskCount === 0) {
               deferral.resolve();
           }
       });

       gulpInst.on('task_err', function (e) {
           var msg = formatError(e);
           var time = prettyTime(e.hrDuration);
           gutil.log(
               '\'' + chalk.cyan(e.task) + '\'',
               chalk.red('errored after'),
               chalk.magenta(time)
           );
           gutil.log(msg);
           process.exit(1);
       });

       gulpInst.on('task_not_found', function (err) {
           gutil.log(
               chalk.red('Task \'' + err.task + '\' is not in your gulpfile')
           );
           gutil.log('Please check the documentation for proper gulpfile formatting');
           process.exit(1);
       });


       gulp.start('vs');

       return deferral.promise;
   }

Putting it all together:

    gulp vs
    cordova platform add android
    cordova build android
    cordova run android