Devlog

Backbone in Titanium

Using Backbone in Titanium Alloy is a very powerful mechanism but it can backfire on you if you don’t take into account how it works.

Remove event listeners from singletons

A common pattern is to create a singleton when a window opens and attach a change event handler to the model so the views will update when something changes on the model.

var member = Alloy.instance("Member");
member.on("change", onChange);

function onChange() {
  console.log("Changed!");
}

The first time everything will work as expected: when somethings changes on the model, you’ll see “Changed!” in the console. But the second time the window opens you’ll see the change event handler will fire 2 times and a third time it fires 3 times. The reason is that a singleton always return the same instance, so every time the window opens, the same event handler is attached to this instance again.

To remedy this, always add event listeners to singletons in the open event and remove them in the close event of the window.

var member = Alloy.instance("Member");

function onChange() {
  console.log("Changed!");
}

function onWindowOpen() {
  member.on("change", onChange);
}

function onWindowClose() {
  member.off("change", onChange);
}

This is only necessary for singletons, so if you create a model with Alloy.createModel, you don’t have to remove them.

Using the properties sync adapter

One of the build-in sync adapter is the properties sync adapter. This will sync the Backbone models to Ti.App.Properties. I use it to save user settings. One thing to remember is to set an id on the model to make sure you get the same instance back.

For example the following will not get you the same instance:

// Model Member.js
exports.definition = {
 config: {
   adapter: {
     type: "properties",
     collection_name: "Member"
   }
 },
 // etc...
};

// Code
var Member = Alloy.instance("Member");
member.fetch({
  success : function() {
    // Do some things with it and then save
    member.save();
  }
});

This code saves the model to a property like “Member-a10ca082-2f10-cd4e-080f-fab1670f6954” where “a10ca082-2f10-cd4e-080f-fab1670f6954” is the randomlt created id of this model. The next time you run this code, another model is fetched (because no id is known) and the model is saved to another property.

There are two ways to solve this:

Add a default id:

exports.definition = {
  config: {
    defaults : {
      id : 1
    }, 
    adapter: {
      type: "properties",
      collection_name: "Member"
    }
  },
  // etc...
};

Or set the id explicitly before fetching the model:

var Member = Alloy.instance("Member");
member.id = 1;
member.fetch({
  success : function() {
    // Do some things with it and then save
    member.save();
  }
});

Using multiple sync adapters

It’s not an unusual demand for an app to be able to save models to the device but also to sync them to an external backend. This means a Backbone model has to be able to sync to 2 adapters: an sql adapter for device sqlite storage and a RESTful adapter for external storage.

A way to implement this is by creating a custom sync adapter that syncs the model to a specific storage based on the options object in the Backbone.sync method. These are the steps to implement it:

Update your model to use this adapter, for example “mysync”:

exports.definition = {
  config: {
    adapter: {
      type: "mysync",
    }
  },
  // etc...
};

Create a custom sync adapter and place it in app/assets/alloy/sync/mysync.js.

module.exports.sync = function(method, model, options) {
  switch (method) {
    case "read":
      if (options.isRestful) {
        // save to RESTful API
      } else {
        // save to sqlite
      }
  }
};

Now you can fetch it from the RESTful API like this:

model.fetch({
  isRestful : true,
  success : function() {
    // Model is read from RESTful API
  }
});

Arrays as model properties

If you have a model with arrays as properties, changing items in this array won’t trigger a change event on the model. This is because the property is still pointing to the same array. So you have to manually trigger a change event:

var orders = myInstance.get("orders");
var order = {price : 12.95, date : "2014-12-30"};
orders.push(order);
myInstance.trigger("change");
// Or:
myInstance.trigger("change:orders");
// Or:
myInstance.trigger("change:orders", order);

When saving a Date object in the properties on Android, it is still returned as a string when calling getObject:

var date = new Date();
Ti.App.Properties.setObject("date", date);
var date2 = Ti.App.Properties.getObject("date");
console.log(typeof date2);
// On Android "string", on iOS "object"
// So you have to do:
if (typeof date === "string") {
  date2 = new Date(date2);
}