Offline Data Synchronization in Ionic

Holding phone

Your application needs to work with a remote database but be capable of working offline? Here's how to do it in Ionic.

Cartoon - C'mon! Just need a tiny little network bar to send this email!

A data-driven mobile application capable of working even when the device has no internet connection is a must nowadays. Look at Gmail app. Without internet connection you can read and create emails and the app will deliver them as soon as the device gets connected. Imagine working with the email app only when there’s connection. That’s not so useful, right?

The principle behind this solution is quite simple. We have on local database at application side and when the app is connected, it downloads or synchronizes with the remote database. If disconnected, it just waits for the next synchronization while normal operations still can be done on the application.

Architecture

One way to implement this solution is using a 4-way data binding architecture. Many Javascript frameworks like AngularJS, Ember.js or Knockout implement 2-way data binding which is a automatic synchronization of data between the model and the view components.

2-Way Data Binding

So, to implement a 4-way data binding we just need to add a data synchronization between the model component and the local database and between the local database and a remote database.

4-Way Data Binding

Actually, if we wanted to be accurate, we should call it 6-way but 4-way is the convention that many people are using.

On this article I’ll show you how we can quickly build a simple ToDo mobile application with a similar architecture as Gmail app. I chose to build it as an hybrid mobile application.

You probably noticed that there are so many different frameworks out there that allows us to build a mobile application using web technologies like HTML5, CSS and Javascript. Among them there’s Ionic. Based on Angular JS from Google, Ionic is an open-source framework that runs on top of Cordova to generate apps both for IOS and Android platforms.

Personally I find Ionic one of the most flexible frameworks that I’ve tried and definitely one of the easiest to learn. There’s a lot of resources on the internet and Drifty, the company behind it, gives a great support.

To store our application data we’ll use Apache CouchDB, a database for the web, as their authors claim. It is a No-SQL database that stores documents in JSON and uses HTTP for an API. To handle local storage, we’ll take advantage of PouchDB which is practically a CouchDB clone designed to run within a browser. It’s a perfect fit for our solution since hybrid applications rely on webviews, which is basically a browser inside the app. PouchDB also provides easy and smart synchronization with remote CouchDB databases.

Install CouchDB

There are a few providers for online CouchDB databases that you can use but I’ll show you how to quickly set it up on a Ubuntu 13.04 server. I believe most of the steps can replicated on other versions and even on Debian systems. There are several tutorials available for other systems. Check out CouchDB documentation.

Start by updating Ubuntu’s package manager:

$ apt-get update

Then install the tools required to compile CouchDB:

$ apt-get install -y build-essential

Install erlang and a few other dependencies:

$ apt-get install -y erlang-base erlang-dev erlang-nox erlang-eunit

Finally, install a few libraries that CouchDB needs:

$ apt-get install -y libmozjs185-dev libicu-dev libcurl4-gnutls-dev libtool

Time to compile

Now we have all that is needed to build CouchDB. Let’s download the source. Go to:

$ cd /usr/local/src

Download the source:

$ curl -O http://apache.mirrors.tds.net/couchdb/source/1.6.1/apache-couchdb-1.6.1.tar.gz

Unarchive the files:

$ tar xvzf apache-couchdb-1.6.1.tar.gz

Go to the new directory:

$ cd apache-couchdb-1.6.1.tar.gz

Compile and install:

$ ./configure

$ make && make install

Setup CouchDB

Let’s create an user for CouchDB:

$ adduser --disabled-login --disabled-password --no-create-home couchdb

Just press enter on the following prompts if you like.

Setup the directories permissions:

$ chown -R couchdb:couchdb /usr/local/var/log/couchdb /usr/local/var/lib/couchdb /usr/local/var/run/couchdb /usr/local/etc/couchdb

Install CouchDB as a service and make it run on boot:

$ ln -s /usr/local/etc/init.d/couchdb  /etc/init.d

$ update-rc.d couchdb defaults

Start the service and relax™!

$ service couchdb start

Make sure it’s working:

$ curl localhost:5984

You should see something like this:

$ curl localhost:5984
​{"couchdb":"Welcome","uuid":"d79a7c37116364fcc76bcb91901f48c6","version":"1.6.1","vendor":{"name":"The Apache Software Foundation","version":"1.6.1"}}

Wait, there’s more…

By default, CouchDB is only accessible locally on your server. If it’s ok by you, leave it like it is. If not, there is a few more steps to take.

Let’s change the configuration file. But first make a backup:

$ cp /usr/local/etc/couchdb/local.ini /usr/local/etc/couchdb/local.ini.bak

Open it up on an editor:

$ nano /usr/local/etc/couchdb/local.ini

Look for the bind_address setting and change it from 127.0.0.1 to 0.0.0.0 and enable CORS:

[httpd]
port = 5984
bind_address = 0.0.0.0
enable_cors = true

Also add CORS settings:

[cors]
credentials = true
origins = *
methods = GET, PUT, POST, HEAD, DELETE
headers = accept, authorization, content-type, origin, referer, x-csrf-token

With those settings, your database server will be able to accept external connections and without CORS issues.

If you're having issues setting CORS, you can also try the add-cors-to-couchdb script.

Then restart CouchDB:

$ service couchdb restart

You can access CouchDB admin UI via browser at http://[your address]:5984/_utils

Please keep in mind that I’m not going to cover any security details. A configuration like this will leave your database server open to external access if it’s public. Check CouchDB documentation for further details on security.

And now, the good stuff.

Create the application

I’m assuming that you already have NodeJS installed. If you still haven’t install Ionic framework, on your terminal:

$ npm install -g cordova ionic

This will also install Cordova API set together with Ionic.

Let’s start an empty project:

$ ionic start todoApp blank
$ cd todoApp

And install the platforms you would like to build to:

$ ionic platform add ios
$ ionic platform add android

Needless to say that you need to have XCode for iOS or Android SDK for Android installed on your machine.

Here’s a tip: For Android builds I strongly recommend you use Crosswalk. In simple terms it’s an enhanced Android webview that can be compiled with your Ionic app. Crosswalk is built upon the latest versions of Chromium project, the same used by Google’s Chrome and enables your app to run without any issues on a pretty wide range of Android devices, even on ancient ones.

To add it to your project, Ionic makes it incredibly easy:

$ ionic browser add crosswalk

Time to add PouchDB to our project:

$ bower install pouchdb

Then, on the index.html located at www directory, add the following line:

<script src="lib/pouchdb/dist/pouchdb.min.js"></script>

Since this is a simple Todo application, we’re not going to use any fancy application structure. We’ll just use the www/js/app.js file for all our code and www/index.html for our view. Go ahead, open www/index.html.

Let’s place a list of todos on our app. Add the following html block inside the <body> (replace the existing <ion-pane>):

   <ion-pane ng-controller="TodoController">
     <ion-header-bar class="bar-stable">
       <h1 class="title">ToDo</h1>
       <button class="right button button-icon icon ion-plus" ng-click="create()"></button>
     </ion-header-bar>
     <ion-content>
       <ion-list>
         <ion-item ng-repeat="todo in todos | orderBy:'done'">
           <ion-checkbox ng-model="todo.done" ng-change="update(todo)">{{todo.title}}</ion-checkbox>
         </ion-item>
       </ion-list>
     </ion-content>
   </ion-pane>

You can see by this code that the UI will have a button to create todos and a list of todo objects, each one with a checkbox that updates the todo attribute done and triggers an update function.

You can close the index.html file and open up the www/js/app.js file.

At the top of the file add these two global variables:

var localDB = new PouchDB("todos");
var remoteDB = new PouchDB("http://[your database server ip]:5984/todos");

As you probably guessed, this will declare our local and remote databases.

Now, for a better following of the next steps, change the following line:

angular.module('starter', ['ionic'])

to:

var app = angular.module('starter', ['ionic'])

And add a semicolon at the end.

Inside the

.run(function($ionicPlatform) {

block and just before

$ionicPlatform.ready(function() {

insert the following code:

localDB.sync(remoteDB, {live: true, retry: true});

All the local/remote data synchronization magic happens here and it’s only one line of code! Using the sync method with live:true setting, all data will be synchronized on-the-fly. The retry:true setting will insure that the synchronization resumes after the app has been offline.

Now, we need to watch any changes on the local database and update our todos list. With PouchDB, we can use changes() method and make it trigger some events using $rootScope.$broadcast. So let’s add a factory to our module. For more information about $broadcast and factories, you can check AngularJS docs. Just add the following code at the end:

app.factory('PouchDBListener', ['$rootScope', function($rootScope) {
 localDB.changes({
   continuous: true,
   onChange: function(change) {
     if (!change.deleted) {
       $rootScope.$apply(function() {
         localDB.get(change.id, function(err, doc) {
           $rootScope.$apply(function() {
             if (err) console.log(err);
             $rootScope.$broadcast('add', doc);
           })
         });
       })
     } else {
       $rootScope.$apply(function() {
         $rootScope.$broadcast('delete', change.id);
       });
     }
   }
 });
 return true;
}]);

So the above code tells us that whenever a change is made on the database it will broadcast an add event in case some todo was not deleted (updated/inserted) otherwise it will broadcast a delete event.

Good stuff. Time to write our controller which will look for changes and create todos through an $ionicPopup prompt. Add this code to your www/js/app.js file:

app.controller("TodoController", function($scope, $ionicPopup, PouchDBListener) {
 $scope.todos = [];
 $scope.create = function() {
   $ionicPopup.prompt({
     title: 'Enter a new TODO',
     inputType: 'text'
   })
   .then(function(result) {
     if(result) {
       if($scope.hasOwnProperty("todos") !== true) {
         $scope.todos = [];
       }
       localDB.post({title: result, done: false});
     } else {
       console.log("Action cancelled.");
     }
   });
 }

 $scope.update = function(todo) {
   localDB.put({
     _id: todo._id,
     _rev: todo._rev,
     title: todo.title,
     done: todo.done
   })
   .then(function(result){
     // You can set some action after the item was updated.
   });
 }
 $scope.$on('add', function(event, todo) {
   var add = true;
   angular.forEach($scope.todos, function(value, key) {
     if (value._id == todo._id) {
       $scope.todos[key] = todo;
       add = false;
       return;
     }
   });
   if (add) {
     $scope.todos.push(todo);
   }
 });

 $scope.$on('delete', function(event, id) {
   for(var i = 0; i < $scope.todos.length; i++) {
     if($scope.todos[i]._id === id) {
       $scope.todos.splice(i, 1);
     }
   }
 });
});

All done, time to test our application:

$ ionic serve

See the Pen rVOLvW by Marco Fernandes (@marcofernandes) on CodePen.

Build and run the application on your favorite platform:

$ ionic build ios
$ ionic run ios

or:

$ ionic build android
$ ionic run android

Using Ionic’s run command, it will try to run the app on a connected device. If none found it will try to open up the emulator.

In the following video, you can see our application running in all its glory in several virtual devices:

Looks pretty good.

Now you are probably wondering "Hey! I can't remove todos!". Yes, I leave to you to implement that "nice-to-have" feature. Consider it as an exercise. Here's a tip: http://ionicframework.com/docs/api/directive/ionList/.

Feel free to post a comment below with your results.

Resources: IonicPouchDBCouchDBAngularJSCordova.