An Architecture for PhoneGap Apps with AngularJS

PhoneGap

PhoneGap is a well known framework for constructing multi-platform mobile apps using web technologies (html, js, css). A Single Page Architecture (SPA) for a PhoneGap app has significant benefits. It makes it easy to keep a global context. Asynchronous calls are better handled using such a global context. Transitions between screen switching are performed by replacing views and hence are much faster.

AngularJS

AngularJS is a client-side MVC that has become a prominent framework for SPA. AngularJS provides two-way data binding between views and models, and view routing using ngRoute. One can animate app activities such as view transitions using ngAnimate.

PhoneGap with AngularJS

Marrying PhoneGap and AngularJS seems natural, especially as AngularJS can enhanced for touch with Angular Touch. PhoneGap interacts with the mobile device capabilities such as GPS location using plugins. ngCordova is a set of AngularJS extensions on top of the PhoneGap API to make it easier to build PhoneGap apps with AngularJS.

The Architecture

Along several projects, we have evolved a common structure for a PhoneGap app using AngularJS and ngCordova.

The www folder structure is:

js
  app.js
  directives.js
  controllers.js
  services.js
  filters.js
partials
  topbar.html
  bottombar.html
sass
  style.scss
css
  style.css
img
  view1
     img11.png
     img12.png
     ...
 view2
    ...
    ...
views
   view1
     view1.js
     view1.html
view2
  view2.js
  view2.html
  ...
index.html 
bower.json

To avoid cluttering the views and img folders we divide them in correspondence by screens.

The Frameworks

As described above, we use AngularJS, ngRoute, ngAnimate, ngCordova, and Angular Touch.

UX Frameworks

We often use Foundation but Bootstrap can be used instead as well. We use Font Awesome for common icons. We use Angular Loading Bar to show a spinner and progress bar automatically by hooking into $http.

Useful Development Tools

To streamline development, we use Bower to include JavaScrip dependecies and Sass to generate css. For more details, see my post Useful Tools for PhoneGap Apps.

Bower

Here is a useful bower.json including all of the above frameworks and some more to be described.

{
  "name": "myHybrid",
  "description": "PhoneGap mobile App",
  "version": "1.0.0",
  "homepage": "http://yoramkornatzky.com",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "latest",
    "angular-route": "latest",
    "angular-animate": "latest",
    "angular-touch": "latest",
    "ngCordova": "latest",

    "ngstorage": "latest",
    "taffydb": "latest",

    "foundation": "latest",
    "fontawesome": "latest",
    "angular-loading-bar": "latest",

    "underscore.number": "latest",
    "underscore": "latest",
    "underscore.string": "latest",

    "validator-js": "latest"
  }
}

The AngularJS App

The app has the standard AngularJS structure:

angular.module('myApp', [
  'ngRoute',
  'ngCordova',
  'ngTouch',
  'angular-loading-bar',
  'myApp.view1',
  'myApp.view2',
  'myApp.filters',
  'myApp.controllers',
  'myApp.services',
  'myApp.directives'
]).
config(['$routeProvider', function($routeProvider) {
  $routeProvider.otherwise({redirectTo: '/entrance'});
}]);

Bootstrapping the App

We hook to the deviceReady event:

var onDeviceReady = function() {   
  console.log("onDeviceReady");
  angular.bootstrap(document, ['myApp']);
};

document.addEventListener('deviceready', onDeviceReady);

We do not use ng-app!!!

The Single Page

The single index.html includes the required files in the standarad AngularJS way.

Directives

Directives define common elements such the top bar and bottom bar common in many mobile apps.

As an example, we define a top bar which has a title, a back button, and a main menu button with this html, where we use Foundation and Font Awesome,

And a directive:

var app = angular.module('myApp.directives', []);
app.directive('topbar', function ($log, $location) {
  return {
    restrict: 'A',
    replace: true,
    scope: {
      title: "@",
      previous: "&",
      kind: "@"
    },
    templateUrl: 'partials/topbar.html',
    controller: function($scope) {
      $scope.goMainMenu = function() {
        $location.url("/main");
      };
      $scope.goPrevious = function() {
        $scope.previous();
      };
    }
  };
});

We pass to the directive a function to be used for the previous button. The directive is to be used as:

The html's of directives are in the partials folder.

Views

A view is defined with a controller that uses a corresponding template,

 'use strict';

 angular.module('myApp.taxis', ['ngRoute', 'ngCordova',      'ngTouch',
  'angular-loading-bar'])

 .config(['$routeProvider', function($routeProvider) {
   $routeProvider.when('/taxis', {
     templateUrl: 'views/taxis/taxis.html',
     controller: 'taxisCtrl'
   });
  }])

.controller('taxisCtrl', ['$scope', '$log', '$window',  '$location',
  function($scope, $log, $window, $location) {
    $log.debug("taxisCtrl");
    $scope.clickPrevious = function() {
      $window.history.back();
    };
 }]);

where you can see the clickPrevious function passed to the directive.

Utilities

For efficient iterations on collections (arrays, hashes) we use underscore. For convenient manipulation of strings we use Underscore.string. For convenient manipulation of numbers we use Underscore.number. To validate and sanitize values such as email we use validator.js.

Services

We use services for three purposes:

  1. Reusable constants
  2. Keeping an app-wide context
  3. Performing common functions

Reusable Constants

Such constants include:
1. URLs for server-side and external services
2. Options lists for selection
3. Strings translations

For lack of space, we show the urlService only:

app.value('urlService', {
  apiCallsUrl: "http://xxx.xxx.xxx.xxx",
  imagesUrl: "http://yyy.yyy.yyy.yyy"
});

Common Functions

Such common functions include:

  1. Networking with the server and external services
  2. Controlling device capabilities, such as geo location
  3. String translations

For lack of space, we demonstrate only the networking service and geo location tracking.

Networking Service

To perform API calls to the server, we use $http, but $resource can be substituted. Calls return promises to be used in views. We assume the server returns a JSON response of the form:

{ "code" : , "data" : , "message" :  }

where any code different from zero is considered an error

app.factory('networkService', function( $http, $q, $log,  urlService,
  stateService){

// common error and success functions
function handleError( response ) {
  // weird format, must be an error
  if (!angular.isObject( response.data ) || !response.data.message)
    return( $q.reject( "An unknown error occurred." ) );
    return( $q.reject( response.data.message ) );
  }
  function handleSuccess( response ) {
    if (response.data.code)
      return( $q.reject(response.data.message) );
    return(response.data.data);
  }

  return ({
    createReservation: function(latitude, longitude){
      var request = $http({
        method: "post",
        url: urlService.apiCallsUrl + "reservation/create",
        data: {
          latitude: latitude,
          longitude: longitude
        }
      });
      return( request.then( handleSuccess, handleError ) );
    },
    ...
  });
});

Geo Tracking

We use ngCordova to interact with the geo location plugin of PhoneGap:

app.factory('geoLocationService', function($log, $cordovaGeolocation) {
  var geoLocationWatch = null;

  return {
      startLocationTracking: function() {
        if (!geoLocationWatch){
          var options = {
            enableHighAccuracy: true,
            frequency: 1000,
            maximumAge: 3000,
            timeout: 5000,
          };
          // begin watching
          geoLocationWatch = $cordovaGeolocation.watchPosition(options);
          geoLocationWatch.promise.then(
            function() { /* Not used */ },

            function(err) { /* An error occurred */ },

            function(position) {
              // Active updates of the position here
              // position.coords.[ latitude / longitude ]
            }
          );
        }
      },

      ...
  });
});

App-Wide Context

Along several apps, we found that context has several common patterns, and we define a service for each:

Persistent State

For authentication, push notifications, and user preferences, we need persistent storage. We use ngStorage to get localStorage in the AngularJS way.

We define a stateService, where we show how to persist the userid and token used for authentication:

  var app = angular.module('myApp.services', ['ngStorage']);

  app.factory('stateService', function($log, $localStorage){

    return ({

      setUserIdentification: function(userId, token) {
        $localStorage.userId = userId; 
        $localStorage.token = token;
      },

      getUserIdentification: function() {
        return ({ 
          userId: $localStorage.userId,
          token: $localStorage.token
        });
      },

      eraseUserIdentification: function() {
        delete $localStorage.userId;
        delete $localStorage.token;
      }
    });

  });

Data Filtering and Screen Transitions

A mobile app often has the master-detail (aka list-item) structure, and we need to maintain context when switching from a list to an item. When lists can be filtered by various condions, such as "Show My Friends Only", a filter has to be stored.

We define a filterService to store such non-persitent context:

 app.factory('filterService', function($log){
   var friendId = null;
   var imageData = null;
   var filterCondition = null;

   return ({
     setFriend: function(id) {
       friendId = id;
     },
     getFriend: function() {
       return friendId;
     },
     setFilterCondition: function(condition) {
       filterCondition = condition;
     },
     ...
     getImage: function() {
       return imageData;
     }
   });
 });

imageData is used to store Base 64 encoding of photos when transitioning between a capture screen and a subsequent presentation screen.

Database/List Service

To manage contents of lists, including filteringn by conditions, we use an in-memory database, TaffyDB. In other applications, we might use SQLite.

We define a dbService, using generic functions (with two tables, favorites, and taxis, for instance):

 app.factory('dbService', function($log){

   var favorites = TAFFY();
   var taxis = TAFFY();

   return ({
     eraseDb: function(dbName, value) {
       switch(dbName)
       {
         case "favorites":
           favorites().remove();
         break;
         ...
       }
    },

    fillDb: function(dbName, data) {
      switch(dbName)
      {
        case "favorites":
          taxis.insert(data);
        break;
        ...
      }
   },

   getItemFromDb: function(dbName, itemId) { // select row by id
     switch(dbName)
     {
       case "favorites":
         return favorites({ "id" : itemId }).first();
       break;
       ...
     }
   }
  });
});

Paging/Batch Service

As we page through lists of items, and fetch items in batches, we maintain context with a batch service, in this example for two lists:

app.value('batchService', {
  favoritesListOffset: 0,
  taxisListOffset: 0,
  ...
  sizeTaxisBatch: 8
});

The Code

The source code, a skeleton for the www folder of a PhoneGap app, is available on GitHub angularjs-phonegap-framework.

Future Work

In a subsequent post, we will show how to use the architecture with Back& Backend as a Service (BAAS) which offers an ORM for MySQL, PostgreSQL, and MS SQL Server.