The case against Ti.App.fireEvent

In code-reviews I see a lot of people using Ti.App.fireEvent() and Ti.App.addEventListener() to fire and listen to custom events. This is often used to send instructions between different parts of the app that have no direct reference of each other. Because everyting has access to global objects like Ti.App, it’s indeed easy to use it as a global event dispatcher.

However, there are 2 good reasons not to use it:

  1. You can easily forget to also call Ti.App.removeEventListener() at the right moment, creating a memory leak for all objects and closures in the scope of the listener.
  2. Every use of a Ti. proxy means crossing the bridge to native-land, which involves coverting anything you sent as the second argument of Ti.App.fireEvent(name[, event]) into a native object (JAVA HashMap or Objective-C NSDictionary), which is expensive.

Fortunately there are good alternatives as well.

In an ideal world any controller/module is fully de-coupled; it doesn’t require any knowledge about other parts of the app while properly handling all of its internal housekeeping (references, timers, events..). This doesn’t fit the use of app-wide events very well. Always use callbacks or local events if you can.

1. Use callbacks

The first pitfall can be avoided by using callbacks where possible.

Events are really not that much more then a set of organized callbacks anyway. Anywhere you have a direct causal relationship you can probably better use a callback.

An advantage of callbacks is that the user doesn’t need to (or even can) remove them. The receiver is responsible for making sure the callback can be garbage collected in time. It should not keep references to the callback outside his own scope.

foo.js

var bar = require('bar');

bar.logOut(function onLogOut(res) {

    // probably re-organize your UI a bit

    alert('You' + (res.succes ? 'are' : 'are not ') + ' logged out');
});

bar.js

var callb;

exports.logOut = function logOut(cb) {

    // do something really complex and probably async

    cb({
        success: true
    });

    // some bad ideas:

    // callb = cb;
    // Ti.App.addEventListener('foo', cb);
};

2. Use JS-only events

If there’s no direct relationship between the action and result, you probably do need events, but certainly not Ti.App.fireEvent.

Controller === Event Dispatcher

Did you known that every Alloy controller is an event dispatcher? It extends Backbone.Events enabling you to use $.trigger() and ctrl.on(). If you don’t use Alloy, you can of course still extend Backbone’s Event dispatcher like we do in the last examle or create your own.

These JS-only events don’t cross the bridge and Backbone throws in some nice extras like ctrl.off() which can even remove all listeners to (a certain event of) the object.

BackBone 1.x has even more event-related goodness like once() and listenTo() but at the moment Alloy still ships with 0.9.2.

foo.js

var bar = Alloy.createController('bar');

bar.on('logOut', function onLogOut(e) {

    // so bar doesn't keep us from GC
    bar.off('logOut', onLogOut);

    // so we don't keep bar from GC
    bar = null;
});

bar.js

function onLogOutButtonClick(e) {
    $.trigger('logOut');

    // we can also free ourself from listeners
    // $.off();
}

A global JS-only event dispatcher

In both of the examples we saw so far foo.js held a reference to bar.js, but in my introduction I explained Ti.App.fireEvent() is mostly used where this is not the case.

In these cases, you can (and should) still get rid of Ti.App.fireEvent by creating a global JS-only event dispatcher.

dispatcher.js

module.exports = _.clone(Backbone.Events);

// or
// _.extend(existing_export, Backbone.Events);

foo.js

var dispatcher = require('dispatcher');

dispatcher.on('logout', function onLogOut(e) {

    // so the dispatcher doesn't keep us from GC
    dispatcher.off('logOut', onLogOut);
});

bar.js

var dispatcher = require('dispatcher');

function onLogOutButtonClick(e) {
    dispatcher.trigger('logout');
}

3. Minimize the payload

I can’t think of one, but if you really have to use Ti.App.fireEvent, then at least minimze the payload so the proxy has less to convert. This is also a good practice if you use a JS-only dispatcher by the way. Don’t sent a model (large object) if you can also just sent the its id and have the receiver lookup the model.

controller_a.js

function onListViewClick(e) {
    Ti.App.fireEvent('app:change'
        // model: Alloy.Collections.mine.get(e.itemid);
        id: e.itemid;
    });
}

controller_b.js

// never use anonymous listeners that you can't remove
Ti.App.addEventListener('app:change', function onChange(e) {
    // model = e.model;
    id = e.id,
    model = Alloy.Collections.mine.get(e.itemid);
});

$.win.addEventListener('close', function onClose(e) {
    Ti.App.removeEventListener('app:change', onChange);
});

I hope I’ve convinced you not to use Ti.App.fireEvent anymore and role your own dispatcher instead. This should increase your app’s performance and maybe stop some leaks.

Code strong!

App imagineer: Imagining, Engineering & Speaking about Native mobile Apps with Appcelerator Titanium & Alloy • Meetup organizer • Certified Expert • Titan


Comments

  • Timan Rebel

    In the callback example, shouldn’t it be bar.logOut()?

  • Fokke Zandbergen Post author

    Good catch! Thanks Timan

  • Patrick Seda

    Just curious … due to the nature of WebViews, will Backbone events pass across?

  • Fokke Zandbergen Post author

    No, the only way to communicate with and from a WebView is indeed to use Ti.UI.WebView.fireEvent().

  • Andrew McElroy

    This article does make some interesting points.
    That said, maybe it is time we revisit the titanium guide on handing orientation change.
    http://docs.appcelerator.com/titanium/3.0/#!/guide/Orientation

    It calls using FireEvent a best practice.

    How you would handle orientation change in a non trivial app?

  • Vinicius Oliveira

    e.source.removeEventListener(e.type, arguments.callee);

    This code above works to remove a event ?

  • Ethan

    Great post! Exactly what I was looking for. Thanks Fokke!

    However, on codestrong 2012 app, I can see that they’re using Backbone events to catch click event like in this page : https://github.com/appcelerator/Codestrong/blob/master/app/controllers/profile.js
    > $.avatar.on(‘click’, function() {…

    Then is replacing all addeventlistener(‘click) (for example) by backbone events is a good thing to do?
    What about the native bridge?

  • Fokke Zandbergen Post author

    Did you test that? That might!

  • Fokke Zandbergen Post author

    That’s really early Alloy code where you could also use `on` instead of `addEventListener` on proxy views. So it’s not using BackBone. The proxy views are no real JS objects and have no JS-only event dispatcher like BB does.

  • Fokke Zandbergen Post author

    I would replace

    Ti.App.fireEvent(‘orient’, {portrait:e.source.isPortrait()});

    With a call to a JS-only app-wide event dispatcher like discussed above.

  • Richard Lustemberg

    great article! And also is good to know that with Alloy, any custom object can be extended into an event emitter very easily as Backbone.Event is global, so you don’t even need to include any library to get your objects to listen and fire events.

  • Fokke Zandbergen Post author

    I just tested it… it works!

  • Abdelrhman

    Hi,
    2. Use JS-only events is not working for me

    current = Alloy.createController(‘login’).getView();
    currentName = “login”;
    $.content.add(current);
    //

    current.on(‘login’, function onLogIn(e) {

    Ti.API.info(‘event say hi ‘);
    alert(“I did it we will replace all events”);
    // so the dispatcher doesn’t keep us from GC
    //dispatcher.off(‘login’, onLogIn);
    });

    $.trigger(‘login’);

  • Steven House

    Great article, great site, great github! I’m a big fan. On topic, I don’t think that this will work to communicated an event from a controller to a web view. Any handy advice for that scenario?

    – SH

  • Steven House

    Skipped right to asking a question that was already asked.
    Answer: ‘Nope’
    But I’ll leave the comment to attest to your awesomeness.

  • Charles

    I was wondering if you could point me towards more information regarding the the expensive cost of native handlers vs their javascript counterparts. As far as I know, there’s no JIT compilation on IOS, so it seems weird to me that it would somehow be more efficient, especially when you consider the fact that the javascript implementation would also have to account for the scope surrounding the event handler at the time it was added. Most of the javascript engines I’ve palyed with in the past backed their javascript objects with native types whenever possible, so logically, the memory overhead should be higher on the javascript side.

    Anyways, I’m not doubting you on this – clearly you’re very knowledgeable of the framework – I just wanted to see if I could find out why this was the case.

  • Fokke Zandbergen Post author

    I think it’s not so much a question of native vs JavaScript, is more that in the case of Titanium, calling a JS proxy object involves both JS-part and the native part. The communication between the two is what we call crossing the bridge and this has proved to be very expensive.

  • Mark Mokryn

    Actually WebViews are a good place for Ti.App.fireEvent/addEventListener, and it’s how I’m integrating a websocket-like library into Titanium.

  • Danny

    Is Ti.App.fireEvent() and Ti.App.addEventListener() really so “expensive”? I have made some benchmark tests (on real device, with Ti.Classic app) and didn’t notice any significant performance or memory differences comparing Ti.App.fireEvent() and an event dispatcher (used your example with Backbone).

  • Fokke Zandbergen Post author

    @Danny Did you test sending across big payloads or small? And did you test on iOS or Android?

  • Danny

    Rather small payloads, just geolocation data (latitude, longitude, accuracy), currently only tested with iOS.

    I am working on an app where I have a “global” Geolocation listener that “forward” the current location (and changes) to appropriate subwindows. That works really stable both with i.App.fireEvent() or Backbone dispatcher.

  • Fokke Zandbergen Post author

    @Danny the problems become evident with really big payloads because the type conversions takes longer. It may work fine for 80% of the use cases, but I consider it best practice to not throw anything over the bridge unless needed.

  • Danny

    Ok, thanks for your article. I think I will use the Backbone dispatcher for my app, shouldn’t do any harm ;)