Tutorial on Apache Cordova: Creating Mobile Apps using Cordova

Note: Since this was originally written, I've updated this article to work with cordova@8.0.0, cordova-android@7.0.0, and cordova-ios@4.5.4.

Mobile applications are becoming ubiquitous, from smartphones and tablets to smartwatches and other emerging wearables. However, creating separate apps for each platform demands significant resources, especially for individual developers or small teams.

This is where Apache Cordova shines. It enables the development of mobile applications using familiar web technologies like HTML5, CSS3, and JavaScript.

PhoneGap, an open-source API for accessing native mobile resources, was created by Nitobi in 2009. Their vision was to empower developers to build mobile applications using web technologies, with the option to incorporate native code for performance enhancements or specific hardware access.

Cordova PhoneGap?

To clarify, there’s no actual “Cordova PhoneGap.” Adobe acquired Nitobi in 2011 and contributed the open-source core to the Apache Software Foundation, where it was renamed Apache Cordova. Think of Cordova as the engine (like WebKit) and PhoneGap as a specific browser built on that engine (like Chrome or Safari).

Initially, the differences between Cordova and PhoneGap were minimal. Over time, Adobe PhoneGap introduced its own proprietary features, while Cordova continues to be supported by the open-source community. This review and tutorial delve into Cordova app development, and while some aspects might apply to PhoneGap, it’s not a PhoneGap-specific guide.

Apache Cordova Capabilities

Cordova essentially has no limitations compared to native app development. It provides a JavaScript API that acts as a wrapper for native code, ensuring consistency across devices. You can picture Cordova as an application container with a web view that fills the device’s screen.

Apache Cordova comes equipped with pre-built plugins for accessing device features like the camera, GPS, and file system. As devices evolve, new plugins are developed to support added hardware.

Importantly, Cordova applications are installed just like native applications. This means building your code for iOS generates an IPA file, Android an APK file, and Windows Phone an XAP file. With careful development, users might not even realize they’re not using a native application.

Apache Cordova Capabilities

Apache Cordova Development Workflows

Cordova offers two primary development paths:

  • Cross-platform workflow: Ideal for deploying on multiple platforms with minimal platform-specific coding. This approach utilizes the Cordova Command-Line Interface (CLI), simplifying configuration and building for various platforms.
  • Platform-centered workflow: Suitable for apps designed for a specific platform. It allows for deeper customization by blending native and Cordova components. This approach can be used for cross-platform development, but it’s more time-consuming.

Starting with the cross-platform workflow is generally recommended. Switching to platform-centered development is easier than the reverse, as the CLI might overwrite customizations during the build process.

Prerequisites and Cordova Installation

Before diving into Cordova, install the SDK for each target platform. This tutorial focuses on Android, but the process is similar for other platforms.

Download the Android SDK from here. Windows users will find an installer, while Linux and macOS users receive an extractable archive. Add the sdk/tools and sdk/platform-tools directories to your PATH environment variable, which Cordova uses to locate necessary binaries.

Ensure you have Java JDK and Ant installed. Set ANT_HOME and JAVA_HOME to their respective bin folders. After installing the Android SDK, set ANDROID_HOME to Android/Sdk. Include all *_HOME locations in your PATH as well.

Once the SDK is installed, use the android command to launch the SDK manager and install the latest tools and Android API. Download essential components, including Android SDK Tools, Platform tools, Build-tools, SDK Platform, Google APIs, system images, sources, and the emulator accelerator. Afterward, create an emulator using android avd.

Cordova CLI relies on Node.js and Git. Download and install Node.js from nodejs.org and Git from git-scm.com. NPM is used for installing Cordova CLI and plugins, while Cordova uses Git to download dependencies.

Finally, run:

1
npm install -g cordova

to install Cordova CLI globally (using only npm install cordova is insufficient).

Here’s a summary of the required packages:

  • Java
  • Ant
  • Android SDK
  • NodeJS
  • Git

Update these environment variables accordingly:

  • PATH
  • JAVA_HOME
  • ANT_HOME
  • ANDROID_HOME

Bootstrapping an Application

With Cordova installed, you can access its command-line utility. Open your terminal, navigate to your desired project directory, and run this command to create a new Cordova project:

1
cordova create toptal toptal.hello HelloToptal 

This command uses the cordova create subcommand, specifying the project folder, application namespace, and display name. It creates a folder structure like this:

1
2
3
4
5
6
toptal/
|-- hooks/
|-- platforms/
|-- plugins/
|-- www/
`-- config.xml

The www folder holds your core application code, shared across all platforms.

Cordova simplifies cross-platform development but allows for platform-specific customization. Avoid directly modifying source files within platforms/[platform-name][assets]/www directories, as they’re overwritten with top-level www files during builds.

Customize application metadata (author, description, etc.) in the config.xml file.

Add your desired platform using:

1
cordova platform add android

If needed, remove a platform later with:

1
cordova platform rm android

Inspecting the platforms directory reveals a new folder for each added platform, containing a copy of the www folder. Customize your app for a specific platform (e.g., Android) by modifying files within platforms/android/assets/www and utilizing platform-specific tools.

Remember that rebuilding with the CLI (for cross-platform development) overwrites platform-specific changes. Utilize version control or make customizations after cross-platform development is complete.

For platform-specific adjustments within the cross-platform workflow, create a merges folder alongside the other top-level directories (hooks, platforms, plugins, and www).

Place customizations within merges/[platform-name]. These changes are applied after the top-level www folder, enabling you to add new files or override existing ones for specific platforms. For instance:

1
2
3
4
5
6
7
merges/         
|-- wp8/        
|    `-- app.js                 
|-- android/        
|    `-- android.js         
|-- www/        
`-- app.js      

In this structure, the output for Android includes both app.js and android.js, while the Windows Phone 8 output only contains app.js from the merges/wp8 folder, overriding the top-level file.

The plugins directory manages plugin information for each platform. At this stage, it likely only contains android.json with a structure similar to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "prepare_queue": {
        "installed": [],
        "uninstalled": []
    },
    "config_munge": {
        "files": {}
    },
    "installed_plugins": {},
    "dependent_plugins": {}
}

Let’s build the application and deploy it. Cordova offers various CLI commands: cordova prepare, cordova compile, cordova build (combining the previous two), cordova emulate, and cordova run (incorporating build and capable of running the emulator). Typically, you’ll use this command to build and run in the emulator:

1
cordova run --emulator

To deploy directly to a USB-connected device with USB debugging enabled, run:

1
cordova run 

This copies files to platforms/* and executes necessary tasks.

Limit the build scope to a specific platform or emulator:

1
cordova run android --emulator

or

1
cordova run ios --emulator --target="iPhone-8-Plus"

Hands-on Apache Cordova Tutorial

Let’s create a simple application demonstrating Cordova and its plugins. The complete demo is available at this GitHub repository.

Using our existing setup, let’s enhance it. We’ll add functionality to add and view projects in a hypothetical Toptal database. Modify index.html to include two tabs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="format-detection" content="telephone=no" />
        <meta name="msapplication-tap-highlight" content="no" />
        <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
        <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
        <link rel="stylesheet" href="css/jquery.mobile-1.4.5.min.css" />
        <link rel="stylesheet" type="text/css" href="css/toptal.css" />
        <title>Hello Toptal</title>
    </head>
    <body>
        <div id="container">
            <div id="tab-content">
                    
            </div>
        </div>
        <footer>
            <ul id="menu">
                <li id="search-tab-button" class="tab-button active" data-tab="#search-tab">Search Projects</li>
                <li id="post-tab-button" class="tab-button" data-tab="#add-tab">Post a Project</li>
            </ul>
        </footer>
        <div id="dev-null" style="display: none"></div>
        <script src="js/lib/jquery-1.11.1.min.js"></script>
        <script src="js/lib/jquery.mobile-1.4.5.min.js"></script>
        <script type="text/javascript" src="cordova.js"></script>
        <script type="text/javascript" src="js/SQLiteStorageService.js"></script>
        <script type="text/javascript" src="js/Controller.js"></script>
        <script type="text/javascript" src="js/index.js"></script>
    </body>
</html>

This incorporates Bootstrap and jQuery Mobile as dependencies. While more sophisticated frameworks exist for building modern hybrid apps, these libraries are familiar to most web developers. Download the stylesheets from GitHub or use your preferred versions.

Next, simplify index.js to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var app = {
    // Application Constructor
    initialize: function() {
        if (navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/)) {
            document.addEventListener("deviceready", this.onDeviceReady, false);
        } else {
            this.onDeviceReady();
        }
    },

    onDeviceReady: function() {
        // We will init / bootstrap our application here
    },
};
app.initialize();

Cordova apps typically benefit from a Single Page Application (SPA) architecture. Resources are loaded once at startup, remaining in the web view, and eliminating page reloads for a smoother, native-like experience. Let’s create a basic controller for switching between our tabs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var Controller = function() {
    var controller = {
        self: null,
        initialize: function() {
            self = this;
            this.bindEvents();
            self.renderSearchView(); 
        },

        bindEvents: function() {
            $('.tab-button').on('click', this.onTabClick);
        },

        onTabClick: function(e) {
            e.preventDefault();
            if ($(this).hasClass('active')) {
                return;
            }
            
            var tab = $(this).data('tab');
            if (tab === '#add-tab') {
                self.renderPostView();
            } else {
                self.renderSearchView();
            }
        },

        renderPostView: function() {
            $('.tab-button').removeClass('active');
            $('#post-tab-button').addClass('active');

            var $tab = $('#tab-content');
            $tab.empty();
            $("#tab-content").load("./views/post-project-view.html", function(data) {
                $('#tab-content').find('#post-project-form').on('submit', self.postProject);
            }); 
        },
       
        renderSearchView: function() {
            $('.tab-button').removeClass('active');
            $('#search-tab-button').addClass('active');

            var $tab = $('#tab-content');
            $tab.empty();

            var $projectTemplate = null;
            $("#tab-content").load("./views/search-project-view.html", function(data) {
                $projectTemplate = $('.project').remove();
                // Load projects here
            }); 
        }
    }
    controller.initialize();
    return controller;
}

We have two methods: one for the Search View and another for the Post Project view. Initialize this controller in index.js:

1
2
// top of index.js
var controller = null
1
2
// inside onDeviceReady method
controller = new Controller();

Add a script reference to Controller.js in index.html before index.js. The Search and Post views can be downloaded from GitHub. Since partial views are loaded from files, browsers like Chrome might issue cross-domain request warnings. Consider using a local static server (e.g., the node-static npm module) or frameworks like PhoneGap or Ionic, which offer tools like browser emulation, hot reloading, and scaffolding.

Deploy to an Android device:

1
cordova run android

You should now have an app with two tabs: one for searching projects:

Apache Cordova application

and another for posting new projects:

Apache Cordova project posted

Currently, it’s a web app within a web view. Let’s leverage some native features. Local data storage is a common requirement. Here are some options:

  • LocalStorage
  • WebSQL
  • IndexedDB
  • Server-side storage accessed via web services
  • Third-party plugins

LocalStorage is suitable for small amounts of data (3-10MB limit). IndexedDB is a better fit for larger datasets. WebSQL is deprecated and not universally supported. Web services align with the SPA model but fail offline, although PWAs and Service Workers can mitigate this.

Numerous third-party plugins extend Cordova’s core functionality. The File plugin allows interaction with the device’s file system. For this example, we’ll use SQLitePlugin for a local SQLite database:

1
cordova plugin add https://github.com/brodysoft/Cordova-SQLitePlugin

SQLitePlugin provides an API for interacting with the device’s SQLite database. Create a basic Storage Service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
SQLiteStorageService = function () {
    var service = {};
    var db = window.sqlitePlugin ?
        window.sqlitePlugin.openDatabase({name: "demo.toptal", location: "default"}) :
        window.openDatabase("demo.toptal", "1.0", "DB para FactAV", 5000000);

    service.initialize = function() {
        // Initialize the database 
        var deferred = $.Deferred();
        db.transaction(function(tx) {
            tx.executeSql(
                'CREATE TABLE IF NOT EXISTS projects ' + 
                '(id integer primary key, name text, company text, description text, latitude real, longitude real)'
            ,[], function(tx, res) {
                tx.executeSql('DELETE FROM projects', [], function(tx, res) {
                    deferred.resolve(service);
                }, function(tx, res) {
                    deferred.reject('Error initializing database');
                });
            }, function(tx, res) {
                deferred.reject('Error initializing database');
            });
        });
        return deferred.promise();
    }

    service.getProjects = function() {
        // fetch projects
    }

    service.addProject = function(name, company, description, addLocation) {
        // add a new project
    }

    return service.initialize();
}

Retrieve the code for fetching and adding projects from GitHub. Add a reference to SQLiteStorageService.js in index.html above Controller.js and initialize it in your controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
initialize: function() {
    self = this;
    new SQLiteStorageService().done(function(service) {
        self.storageService = service;
        self.bindEvents();
        self.renderSearchView();
    }).fail(function(error) {
        alert(error);
    });
}

The service.addProject() method utilizes navigator.geolocation.getCurrentPosition(). Cordova’s geolocation plugin retrieves the device’s location. You can even use navigator.geolocation.watchPosition() for location updates.

Finally, add event handlers to the controller for adding and fetching projects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
renderPostView: function() {
    $('.tab-button').removeClass('active');
    $('#post-tab-button').addClass('active');

    var $tab = $('#tab-content');
    $tab.empty();
    $("#tab-content").load("./views/post-project-view.html", function(data) {
        $('#tab-content').find('#post-project-form').on('submit', self.postProject);
    });
},


postProject: function(e) {

    e.preventDefault();
    var name = $('#project-name').val();
    var description = $('#project-description').val();
    var company = $('#company').val();
    var addLocation = $('#include-location').is(':checked');

    if (!name || !description || !company) {
        alert('Please fill in all fields');
        return;
    } else {
        var result = self.storageService.addProject(
            name, company, description, addLocation);

        result.done(function() {
            alert('Project successfully added');
            self.renderSearchView();
        }).fail(function(error) {
            alert(error);
        });
    }
},


renderSearchView: function() {
    $('.tab-button').removeClass('active');
    $('#search-tab-button').addClass('active');

    var $tab = $('#tab-content');
    $tab.empty();

    var $projectTemplate = null;
    $("#tab-content").load("./views/search-project-view.html", function(data) {
        $('#addressSearch').on('click', function() {
            alert('Not implemented');
        });

        $projectTemplate = $('.project').remove();

        var projects = self.storageService.getProjects().done(function(projects) {

            for(var idx in projects) {
                var $div = $projectTemplate.clone();
                var project = projects[idx];

                $div.find('.project-name').text(project.name);
                $div.find('.project-company').text(project.company);
                $div.find('.project-description').text(project.description);

                if (project.location) {
                    var url =
                        '<a target="_blank" href="https://www.google.com.au/maps/preview/@' +
                        project.location.latitude + ',' + project.location.longitude + ',10z">Click to open map</a>';

                    $div.find('.project-location').html(url);
                } else {
                    $div.find('.project-location').text("Not specified");
                }

                $tab.append($div);
            }
        }).fail(function(error) {
            alert(error);
        });
    });
}

Install the console and dialog plugins:

1
2
cordova plugin add org.apache.cordova.dialogs
cordova plugin add org.apache.cordova.console

The cordova.console plugin enables console.log() for debugging in emulators.

Debug Android apps via Chrome’s remote debugger. Connect your device, open Developer Tools, go to “More Tools” > “Inspect Devices,” select your device, and access the console. Safari offers similar functionality for debugging iOS apps on connected devices or emulators (enable Developer Tools in Safari Settings > Advanced).

The cordova.dialogs plugin enables native notifications. It’s common practice to override windows.alert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
overrideBrowserAlert: function() {
    if (navigator.notification) { // Override default HTML alert with native dialog
        window.alert = function (message) {
            navigator.notification.alert(
                message,    // message
                null,       // callback
                "Toptal", // title
                'OK'        // buttonName
            );
        };
    }
}

Call the overrideBrowserAlert function within the deviceready event handler.

You can now add and view projects in the database. Selecting “Include location” fetches your location using the Geolocation API.

Let’s add an icon and splash screen. Include the following in your config.xml:

1
2
3
4
5
6
<platform name="android">
    <icon src="www/img/logo.png" />
    <splash src="www/img/logo.png" density="mdpi"/>
    <splash src="www/img/logo.png" density="hdpi"/>
    <splash src="www/img/logo.png" density="xhdpi"/>
</platform>

Place a logo image in the www/img folder.

Cordova mobile tutorial application

Your Own Cordova App

This tutorial provided a glimpse into Apache Cordova app development, highlighting its ability to leverage familiar technologies and expedite cross-platform development.

For production-ready applications, consider using an existing framework for structure, pre-built components, and a native-like feel. Noteworthy frameworks include Ionic, Framework7, Weex, Ratchet, Kendo UI, and Onsen UI.

Good luck building your Cordova applications!

Licensed under CC BY-NC-SA 4.0