Skip to content

Alloy and TouchDB Tutorial

Ye Chuah edited this page Jan 18, 2015 · 38 revisions

TouchBooksAlloy Tutorial

The TouchBooksAlloy sample app shows how easy it is to build a cross-platform, sync-enabled application with Alloy and TiTouchDB. The full source code for this tutorial can be found in the samples directory here.

This tutorial is suitable for intermediate-level Titanium developers and assumes that you can build an app and deploy it to a device for testing. Some demonstrations of the app do require a device, but all of the code can be run on the iOS simulator or Android emulator.

Contents

App Design ----------

In this tutorial, we will be building a mobile app that stores a list of books and synchronizes the book list among multiple devices. The app has two windows: one containing a table view with a list of books grouped by author or publication date, and another that allows the user to create a new book entry or edit an existing entry. Each entry will be stored in TouchDB as a JSON document. Documents in TouchDB are schemaless; it is up to us to determine and enforce the structure of what we are storing. Here's how we're defining a "book" document for this tutorial:

{
   "_id": "978-1449379681",
   "title": "CouchDB: The Definitive Guide",
   "author": "J. Chris Anderson",
   "published": [2010,9],
   "modelname": "book"
}

This is obviously a simplification of the metadata about a book -- for example, the structure implies that a book only has one author -- but it will do for the tutorial. One item to note is that the "published" property is expressed as an array containing the year, month, and day of publication. JSON doesn't have an intrinsic type of dates, and most of the time I would recommend storing dates in UNIX timestamp format (the number of seconds since the epoch). Timestamps are easy to convert to JavaScript Date objects and are not relative to any time zone like a calendar date and time. In our app, however, we are more interested in the calendar than the exact time because most books provide publication dates like "2010" or "November 1997". By using an array, we can store the parts of the date that we have and ignore the ones we don't. It is also worth mentioning that we will be using the ISBN number of the book as its unique identifier (the _id field). This will be important later when we start synchronizing data from multiple devices and resolving conflicting edits.

Project Setup -------------

Follow the instructions on the Alloy project site to install Alloy, create a new app, and apply the Alloy structure to your new app. Once you have your app set up, build and run in the iOS simulator using titanium build -p iphone or in the Android emulator using titanium build -p android to ensure that your environment is working.

In addition to the general Alloy setup, we will need to install the TiTouchDB modules for iOS and Android and the alloy-ti_touchdb persistence adapter so Alloy can use TouchDB as a backing database. Open the Home page of this wiki and get the latest version of the module ZIP files appropriate to your platform. Place these files in the base directory of your project (the same directory as your app and Resources folders); they will be unpacked automatically the next time the project is built. Edit tiapp.xml and add the following lines to the <modules/> section:

<module platform="iphone">com.obscure.titouchdb</module>
<module platform="android">com.obscure.titouchdb</module>

To install the persistence adapter, download and install the persistence adapter according to the instructions in that section of the project.

App Initialization ------------------

One of the first things we want to do when the app is launched is to make sure we have an existing TouchDB database for storing books. In alloy, the app/alloy.js file is used to perform pre-UI initialization like this. Create that file in Studio and add the following code:

var titouchdb = require('com.obscure.titouchdb'),
    manager = titouchdb.databaseManager,
    db = manager.getDatabase(Alloy.CFG.books_db_name);

This code loads up the TiTouchDB module and makes sure that the database named "books" has been created. You can see that we're being good citizens and are using the Alloy configuration system (Alloy.CFG) to specify the name of our database. Edit app/config.json and add the database name to the "global" section as follows:

"global": {
  "books_db_name": "books"
}, 

We will be adding configuration parameters to this file as the project progresses.

Later on in this tutorial, we're going add some code to app/alloy.js which will load up the views that we will be using to fetch data. For now, let's get started on some UI.

Book List Window ----------------

The Book List is the first window that the user will see when the app starts. It will consist of a sectioned table view listing all of the books in the database and a table header containing a control to switch between two grouping and sorting methods, author and by publication year.

Alloy has already created a placeholder view in the app named "index". We're going to edit the generated files to create the book list window, starting with the view file. Open the file named app/views/index.xml in Titanium Studio and replace the generated contents with the following markup:

<Alloy>
  <Window id="main" platform="ios">
    <NavigationGroup id="nav">
      <Require src="book_list"/>
    </NavigationGroup>
  </Window>
  <Require id="main" src="book_list" platform="android"/>
</Alloy>

In this view, we create a root window with a navigation controller for iOS, which itself contains the book list window. For Android, we simply import the book list window. By adding the platform attributes to the respective XML elements, we are instructing the Alloy build process to include the correct markup on the platform being built.

Open the app/styles/index.tss file and replace the contents with the following styles:

"#main": {
  backgroundColor: 'white'
}

Edit app/controllers/index.js and replace the contents with the following code:

if (OS_IOS) {
  $.main.open();
}
else if (OS_ANDROID) {
  $.main.getView().open();
}

Ok, why did we have to add that getView() call for Android? Whenever you use <Require/> in an Alloy view file, the resulting object is the controller class, not the view class, so you have to get the controller's view and open it. For iOS, the <Require/> is inside a NavigationGroup object, and it appears in this case that Alloy knows how to handle a required view inside another view. My advice is to just go with the flow on this one.

Next, we need to build the book_list view that we required in the index view. Run alloy generate view book_list from the command line to create the XML and style files. Open app/view/book_list.xml and replace the contents with the following:

<Alloy>
  <Window id="win" class="booklist" title="Books" onOpen="loadBookList">
    <View id="header" platform="ios">
      <TabbedBar index="0" onClick="changeGrouping">
        <Labels>
          <Label>Author</Label>
          <Label>Published</Label>
        </Labels>
      </TabbedBar>
    </View>
    <View id="header" platform="android">
      <Switch id="authorSwitch" titleOn="Author" titleOff="Author" value="true" onClick="changeGrouping"/>
      <Switch id="publishedSwitch" titleOn="Published" titleOff="Published" onClick="changeGrouping"/>
    </View>
    <TableView id="tableView"/>
  </Window>
</Alloy>

In this window, we define a header view to hold the table grouping controls and the table view itself (we don't use the headerView property of TableView due to this bug). Once again, we're using the platform attribute to show a tabbed bar for iOS and a list of switches for Android. Notice that both switches on Android and the tabbed bar are calling the changeGrouping() function in our controller, however, so a single function will handle both UI types.

There are a few styles that we need to apply to the view in order to get everything laid out the way we want. Open app/styles/book_list.tss and replace the contents with the following styles:

".booklist": {
  layout: 'vertical',
  backgroundColor: 'white'
},

"#header": {
  height: '48dp',
  backgroundColor: 'white',
},

"#header[platform=android]": {
  layout: "horizontal"
},

"TableView": {
  height: Ti.UI.Fill
},

"TabbedBar[platform=ios]": {
  bottom: '6dp',
  height: Ti.UI.SIZE,
  width: Ti.UI.SIZE,
  style: Ti.UI.iPhone.SystemButtonStyle.BAR,
},

"Switch[platform=android]": {
  left: '10dp',
  style: Ti.UI.Android.SWITCH_STYLE_TOGGLEBUTTON
}

Now that we have the book list window defined and styled, let's create a controller and implement the two event handler functions from the view XML. Create a file named app/controller/book_list.js and add the following code:

function loadBookList(e) {
  // TODO
}

function changeGrouping(e) {
  // TODO 
}

Right now, there isn't much to look at. Once we have defined our models, we can implement loadBookList() and changeGrouping(). This is enough code to build and run the app, however; in a Terminal window, cd to the project directory and run alloy run (iOS) or alloy run . android (Android). If you are testing on Android, open a new Terminal window and run titanium emulator --platform=android in the project directory to start the emulator (see TIMOB-10072 for a workaround if the emulator won't run). Here's what you should see:

iOS book list window Android book list window

Book List View Functions ------------------------

We're going to head back into database-land now and set up the Alloy models which will provide the data for the book list window. If you haven't done so already, take a look at "Working with Models and Collections" on the Alloy web site for an introduction to using the persistence objects. Alloy ships with persistence adapters to store objects to a properties file, SQLite, or HTML5 LocalStorage. The TouchDB persistence adapter that we installed during the project setup gives us the additional ability to store objects in a local TouchDB database. This adapter maps a Model object in Alloy to a document in TouchDB and a Collection to a TouchDB view. You don't have to know anything about the TouchDB API to work with persistent documents in Alloy, but you do have to be able to create TouchDB views for the persistence adapter to use to fetch collections of model objects.

A TouchDB view is an ordered list of rows that is generated by running all of the documents in the database through a map function. The map function may output one or more rows, each of which consist of a key, a value, and (optionally) the source document used to generate the row. When a view is queried, the rows are returned in the lexicographic order of their keys. Views are automatically updated as source documents are added, modified, and removed. For the book list window, we are going to create two views that correspond to the two sorting options we built into the UI: sort by author and sort by publication year. I find that the easiest way to write view functions is to prototype them in CouchDB running on my Mac (download from the CouchDB site) to make sure they work as expected, then copy them into the app. If you aren't familiar with CouchDB and Futon, the web based admin tool, check out the Tour chapter in the CouchDB Guide. For this tutorial, I'm just going to show the view functions so we can concentrate on the app code:

// books by author map function
function(doc) {
  if (doc.author) {
    emit(doc.author, null);
  }
}

// books by publication year map function
function(doc) {
  if (doc.published && doc.published.length > 0) {
    emit(doc.published[0], null);
  }
}

Our map functions are emitting the document value that we want to sort by as the key and null as the value since we will be using the entire source document to populate the objects in our collection.

Views are defined in TouchDB by registering map and reduce functions with a design document. Fortunately, you don't need to know how to do that, because the TiTouchDB persistence adapter takes care of registering views for your Collections, as we will see in the following section.

Book List Model ---------------

Now that we know how to write our view functions that fetch collection data, let's create the Alloy model files. Run alloy generate model Book titouchdb, which will create a new file named app/models/Book.js. Edit that file and update the configuration to match the following:

config: {
  adapter: {
    type: "titouchdb",
    dbname: "books", // no Alloy.CFG here?
    collection_name: "books",
    views: {
        by_author: { map: "function(doc) { if (doc.author) { emit(doc.author, null); } }" },
        by_published: { map: "function(doc) { if (doc.published && doc.published.length > 0) { emit(doc.published[0], null); } }" }
    },
    default_query_options: {
      prefetch: true
    }
  }
},		

Here's what each of these configuration options means:

  • type (required) the persistence adapter name, must be titouchdb
  • dbname (required) the name of the TouchDB database where this model is stored. I would have expected to be able to use Alloy.CFG.book_db_name, but it didn't work. Sad.
  • collectionName (required) the name of the design document that contains the views that are used to create Collections.
  • views (required) the definitions of the views in the TouchDB database that will correspond to Collections. Each view definition is an object with the following properties:
    • name the name of the view
    • map the map function of the view
    • reduce an optional reduce function for the view
  • default_query_options (optional) any options that we want to use when we access the view. In this case, we are passing prefetch: true to instruct TouchDB to return the full source document with each view row.

Next, we need to tell Backbone how to map a row consisting of a key, value, and optional document into a Model object. Edit the extendCollection function as follows:

  extendCollection: function(Collection) {
    _.extend(Collection.prototype, {
      map_row: function(Model, row) {
        var result = new Model(row.documentProperties);
        // add custom properties here, if any
        return result;
      }
    });
    return Collection;
  }

The map_row function takes the source document properties from the provided view row and uses it to create a new instance of the type that is passed in the Model parameter, which will be a model type appropriate to the collection. Note that the documentProperties property of the row will be null if we didn't set prefetch to true in the view options above.

That about wraps up the TouchDB-specific part of the tutorial! From now on, we'll be working with the Backbone model objects just like you would if you were using one of the built-in persistence adapters.

Populating the Book List ------------------------

Open app/controllers/book_list.js and add the following code to create the books Collection and populate the table view:

var currentView = 'by_author';
var books = Alloy.createCollection("Book");
books.on('fetch', function(e) {
  $.tableView.refresh(books);
});

$.tableView.refresh = function(collection) {
  var data = [];
  collection.each(function(book) {
    var row = Ti.UI.createTableViewRow({
      title: book.get('title')
    });
    data.push(row);
  });
  this.setData(data);
};

function loadBookList(e) {
  books.fetch({ view: currentView });
}

function changeGrouping(e) {
  // TODO
}

In this file, we are creating the collection of book model objects by calling Alloy.createCollection(). The collection starts off empty; we need to call the fetch() method to request that Backbone fetch the collection objects. This call is asynchronous, so we set up an event listener on the collection that runs when the fetch event is received. This listener calls a method that populates the table view's rows.

Now let's implement the changeGrouping() function so we can switch between the two views:

function changeGrouping(e) {
  if (OS_ANDROID) {
    if (e.source.id === 'authorSwitch') {
      if (e.source.value) {
        currentView = 'by_author';
      }
      $.publishedSwitch.value = !e.source.value;
    }
    else if (e.source.id === 'publishedSwitch') {
      if (e.source.value) {
        currentView = 'by_published';
      }
      $.authorSwitch.value = !e.source.value;
    }
  }
  else if (OS_IOS) {
    switch (e.index) {
      case 0:
        currentView = 'by_author';
        break;
      case 1:
        currentView = 'by_published';
        break;
    }
  }
  loadBookList();
}

Leaving aside the code that deals with the platform-specific controls and switch states, the main point of this function is to change the value of currentView and call loadBookList() to re-fetch the collection and trigger an update of the table rows.

These rows are kind of boring, though. Let's create a new view for a custom table row that shows the title of the book and a cover image, if any. Run alloy generate controller book_list_row and edit views/book_list_row.xml, replacing the contents with this markup:

<Alloy>
  <TableViewRow id="tableViewRow" className="bookListRow">
    <ImageView id="cover"/>
    <Label id="title"/>
  </TableViewRow>
</Alloy>

Replace the styles for this view in styles/book_list_row.tss with the following:

"TableViewRow": {
  height: Ti.UI.SIZE,
},

"#cover": {
  top: "8dp",
  left: "8dp",
  bottom: "8dp",
  width: '48dp',
  height: Ti.UI.SIZE
},

"#title": {
  top: "8dp",
  left: "64dp",
  bottom: "8dp",
  right: "8dp",
  textAlign: "left",
  width: Ti.UI.SIZE,
  height: Ti.UI.SIZE,
  font: {
    fontSize: "18dp",
    fontWeight: "bold"
  }
}

The controller file, controllers/book_list_row.js, should contain the following JavaScript:

var book = arguments && arguments[0] || {};

$.title.text = book.get('title');

var att = book.attachmentNamed('cover.png');
var cover = att ? att.content : '/images/text_page.png';
$.cover.image = cover;

$.tableViewRow.addEventListener('click', function(e) {
  Ti.App.fireEvent('books:edit_book', {
    book_id: book.id
  });
});

So where did the attachmentNamed() function come from? The TiTouchDB sync adapter automatically adds the following methods to all Model objects:

  • attachmentNamed(name): fetch the attachment with the provided name as a Ti.Blob
  • addAttachment(name, contentType, content): save an attachment with the specified name and content type. The content parameter is a Ti.Blob.
  • removeAttachment(name): delete the attachment.
  • attachmentNames(): get a list of attachment names (returns an array of strings)

If you add or remove an attachment from a model using one of these method, you should re-fetch the model object.

We haven't implemented the table view sections yet, so let's do that in the refresh() method of controllers/book_list.js:

$.tableView.refresh = function(collection) {
  var data = [];
  var section = null;
  collection.each(function(book) {
    var groupProperty;
    if (currentView === 'by_author') {
      groupProperty = book.get('author');
    }
    else if (currentView === 'by_published') {
      groupProperty = book.get('published')[0];
    }
    
    if (!section || section.headerTitle !== groupProperty) {
      if (section) {
        data.push(section);
      }
      section = Ti.UI.createTableViewSection({
        headerTitle: groupProperty
      });
    }
    
    var row = Alloy.createController('book_list_row', book);
    section.add(row.getView());
  });
  data.push(section); // last section
  
  this.setData(data);
};

Our table view row is now nicely encapsulated in its own Alloy files and even takes care of it's own click event.

Now we're at the point where we need some data. Let's grab some from the cloud...

Pulling Changes From a CouchDB Server -------------------------------------

So far you probably haven't seen anything that particularly distinguishes TouchDB persistence from the built in SQL or properties persistence. That all changes in this section, in which we will add code to enable syncing documents from a CouchDB server.

First, we need a CouchDB server that can be accessed from the Internet. Check out the list of hosting providers on the CouchDB wiki for the latest information about where you can sign up for a hosted CouchDB instance. For this tutorial, I created a new IrisCouch server instance named touchbooks.iriscouch.com. Open Futon, the built-in CouchDB admin tool, by going to the /_utils URI on your server, e.g. http://touchbooks.iriscouch.com/_utils, log in, and create a new database named "touchbooksalloy".

Next, we're going to add code to the app/alloy.js file which sets up continuous replication from the server to the app. We will be adding replication from the app to the server later on in the tutorial when we have support for editing books in the app. Edit app/alloy.js and add the following code:

var pull = db.createPullReplication(Alloy.CFG.remote_couchdb_server);
pull.continuous = true;
pull.addEventListener('status', function(e) {
  if (pull.status == server.REPLICATION_MODE_IDLE) {
    Ti.App.fireEvent('books:update_from_server');
  }
});
pull.start();

Add the remote CouchDB server URL to app/config.json:

"global": {
  "books_db_name": "books",
  "remote_couchdb_server": "http://touchbooks.iriscouch.com/touchbooksalloy"
}, 

IMPORTANT: use your own server hostname and database name, not mine! I periodically mess with my test database, so your app will behave strangely if you're replicating with the above URL instead of your own, private db.

The replication progress listener fires an app event when the number of completed replications increases. Add the following code to app/controllers/book_list.js to listen for that event and reload the list of books:

Ti.App.addEventListener('books:update_from_server', function(e) {
  loadBookList();
});

Run the app and arrange your desktop so you can see the simulator window and your browser window with the Futon site open. In Futon, click on "Overview", then "touchbooksalloy" to view the documents in that database. Add a new document with the following data:

{
   "_id": "978-1449379681",
   "title": "CouchDB: The Definitive Guide",
   "author": "J. Chris Anderson",
   "published": [2010,9],
   "modelname": "book"
}

Futon create doc

Click on the "Save Document" link at the top of the Futon page and watch the simulator. You should see the new book appear within a few seconds:

App new doc

Pick up a couple of books from your real world bookshelf and add them to the database as book documents using Futon. Remember to use the ISBN number as the value of the "_id" attribute -- this will be important later when we sync multiple devices to the same server database. Once you have a couple of documents in the server, try modifying the title of a document and deleting a document using Futon. All of your additions, changes, and deletions will be replicated to the app (if you're on iOS; Android users see the disclaimer above).

Replication is a huge topic, so I suggest the following reading:

  • "CouchDB: The Definitive Guide" has a chapter covering replication
  • The TouchDB wiki chapter on replication describes how the underlying TouchDB library handles replicating with CouchDB servers.
  • The TiTouchDB documentation lists the functions that you call to manage replications and the options for replication instances.

These sites will help you choose the type of replication (continuous vs. one-shot, pull and push) will work best for your application.

iOS Step 2 Android Step 2

Book Details Window -------------------

Moving right along, we are next going to add a window to the app for editing existing book objects. Create a new view by running alloy generate controller edit_book. This will create app/views/edit_book.xml, app/views/edit_book.tss, and app/controllers/edit_book.js. Edit the XML file and replace the contents with the following markup:

<Alloy>
  <Window id="win">
    <ScrollView>
      <Label class="label">ISBN</Label>
      <TextField class="value" id="isbn"/>
      <Label class="label">Title</Label>
      <TextField class="value" id="title"/>
      <Label class="label">Author</Label>
      <TextField class="value" id="author"/>
      <Label class="label">Published</Label>
      <Picker id="published" onChange="changePublished"/>
      <Button id="save" onClick="saveChanges">Save</Button>
    </ScrollView>
  </Window>
</Alloy>

Edit app/styles/edit_book.tss and replace the contents with the following styles:

"Window": {
  backgroundColor: "white",
},

"ScrollView": {
  layout: "vertical"
},

"TextField": {
  height: Ti.UI.SIZE,
  borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
},

"Label": {
  height: Ti.UI.SIZE
},

".label": {
  left: "2%",
  right: "2%",
  top: "10dp",
  textAlign: 'left',
  font: {
    fontWeight: "bold"
  }
},

".value": {
  left: "2%",
  right: "2%",
  top: "6dp",
},

"#published": {
  top: "6dp",
  type: Ti.UI.PICKER_TYPE_DATE  
},

"#save": {
  left: "2%",
  right: "2%",
  top: "6dp",
  bottom: "20dp",
  height: "32dp",
  color: 'white',
  backgroundColor: '#0c0',
  borderRadius: 8,
  font: {
    fontWeight: "bold"
  }
},

"#save[platform=ios]": {
  style: Ti.UI.iPhone.SystemButtonStyle.PLAIN,
  backgroundGradient: {
    type: 'linear',
    colors:[{color:'#0f0',position:0.0},{color:'#0c0',position:0.50},{color:'#090',position:1.0}]
  },
}

Edit app/controllers/edit_book.js and add the following code:

var book = Alloy.createModel('Book');

book.on('fetch', function(obj) {
  $.isbn.value = book.id;
  $.title.value = book.get('title');
  $.author.value = book.get('author');

  var published = book.get('published') || [];
  var publishedDate = new Date();
  publishedDate.setFullYear(published.length > 0 ? published[0] : 1970);
  publishedDate.setMonth(published.length > 1 ? published[1] - 1 : 0);
  publishedDate.setDate(published.length > 2 ? published[2] : 1);
  $.published.value = publishedDate;
});

book.on('update', function() {
  Ti.App.fireEvent('books:book_changed', { book_id: book.id });
  alert(String.format('updated "%s"', book.get('title')));
});

exports.set_book_id = function(id) {
  book.id = id;
  $.isbn.touchEnabled = false;
  $.isbn.borderStyle = Ti.UI.INPUT_BORDERSTYLE_NONE;
  $.isbn.blur();
  book.fetch();
};

function changePublished(e) {
  book.set('published', [e.value.getFullYear(), e.value.getMonth() + 1, e.value.getDate()]);
}

function saveChanges(e) {
  book.id = $.isbn.value;
  book.set('title', $.title.value);
  book.set('author', $.author.value);
  book.save();
}

We are firing an app event named books:book_changed after a book object is created or updated. The book list controller will reload the table view when it receives this event. Add the following code to book_list.js:

Ti.App.addEventListener('books:book_changed', function(e) {
  loadBookList();
});

It is a little inefficient to reload the entire table when only one item has changed. We can't simply update the labels in the existing row in the table view when we receive the change event, however, because the new values of the book fields might affect ordering and sectioning.

Now let's add an event handler to the book list table view rows that fires an app event to open the edit book window:

var row = Ti.UI.createTableViewRow({
  book_id: book.id,
  title: book.get('title')
});
row.addEventListener('click', function(e) {
  Ti.App.fireEvent('books:edit_book', {
    book_id: e.source.book_id
  });
});
section.add(row);

Why are we passing the book identifier instead of the entire book object? While I was initially developing this code, I found that Ti.App.fireEvent() stripped functions from the object before it was delivered to the event listeners. This behavior turned my nice Model object into a plain dictionary of keys and values. I had to re-fetch the model in order to get the save() function, so it made sense to just pass the object ID. In a non-trivial app, I'd probably use postal.js for message passing, which passes full objects between publisher and subscriber objects.

The last part of the edit window is adding the following code to app/controllers/index.js, which opens the edit book window in response to the books:edit_book app event:

Ti.App.addEventListener('books:edit_book', function(e) {
  var editBookController = Alloy.createController("edit_book");
  if (e.book_id) {
    editBookController.set_book_id(e.book_id);
  }
  if (OS_IOS) {
    $.nav.open(editBookController.getView());
  }
  else if (OS_ANDROID) {
    editBookController.getView().open();
  }
});

Run the app and touch one of the table rows on the book list page to see the edit book window:

iOS edit book Android edit book

Change the books title, author, or publication date and touch the "save" button, then use the back button to return to the book list window. You should see your changes reflected in the book list table view.

Pushing Changes To a CouchDB Server -------------------------------------

Syncing our local database changes back to the CouchDB server using push replication is even easier than the pull replication we set up in the earlier section. Add the following code to app/alloy.js:

var push = db.createPushReplication(Alloy.CFG.remote_couchdb_server);
push.continuous = true;
push.start();

Push replications also have progress events, but we don't need to update the app when the server is updated so we aren't listening for them. Run this code and make a change to a book document in the app, then load that document in Futon to see your changes. Even cooler: make changes to a book in the iOS simulator and watch those changes appear in the Android emulator a few seconds later!

One of the compelling features of TouchDB is that the local database is always accessible regardless of network connectivity. Changes that are made when the network is not available will be sent to the server once network connectivity is restored. Let's try this out to see how it works.

  1. Install the TouchBooksAlloy app to your iOS device.
  2. Launch the app and get one or more book documents synced from your CouchDB server.
  3. Open the Settings app and place your device in Airplane Mode.
  4. Go back to TouchBooksAlloy and make one or more changes to existing documents.
  5. Open Futon on your CouchDB server and verify that the modifications have not been made on the server.
  6. Force-quit the TouchBooksAlloy app and relaunch to confirm that the local modifications are persistent.
  7. Turn off Airplane Mode and go back to the TouchBooksAlloy app. After a few moments, verify that your local changes were replicated to the server.
Resolving Conflicting Changes -----------------------------

coming soon

Adding New Books ----------------

We use the edit book window to create a new book as well. On Android, we will put the "add" button in the header of the book list window. Edit app/views/book_list.xml and insert the following markup after publishedSwitch:

<Button id="addButton" onClick="addBook">Add</Button>

For iOS, we will put a system "add" button in the navigation header. Edit app/controllers/book_list.js and add the following code:

function addBook(e) {
  Ti.App.fireEvent('books:edit_book');
}

if (OS_IOS) {
  var addButton = Ti.UI.createButton({
    systemButton: Ti.UI.iPhone.SystemButton.ADD
  });
  addButton.addEventListener('click', addBook);
  $.win.rightNavButton = addButton;
}

Both the Android and iOS buttons call addBook(), which simply triggers the edit event without providing a book ID.

Stuff That Wasn't Covered

  • Attachments: every document can contain opaque (non-indexable) attachments. For this demo, it would be interesting to add a cover image to each book document as an attachment.
  • Web-based apps: The CouchDB server can host web applications as well as documents, which makes it relatively easy to provide your users with a web interface to their data. Tools for building CouchDB-based application include couchapp and Kanso.
  • Peer-to-peer: TouchDB includes an HTTP listener that implements the Couch REST interface. That listener (when it becomes part of TiTouchDB) could be used for peer-to-peer replication when a central server cannot be reached. Jan Lehnhardt is particularly fond of this idea, and when you listen to his talk on the subject you will understand why.
Clone this wiki locally