Cleaning up Alloy controllers

Following up my blog on The case against Ti.App.fireEvent I wanted to share another tip on properly cleaning up Alloy controllers.

If you want to make sure your controller, its view and models are properly cleaned up when you don’t need them anymore there’s 2 best practices I’d like to share.

1. Don’t create references to controller instances

A common practice is to create a controller for every window of your app. You will then probably create an instance of such a controller in an listener to a click on some button like this:

index.js

var someWindow;

function onSomeClick() {

  // bad
  // someWindow = Alloy.createController('someWindow');
  // someWindow.getView().open();

  // good
  Alloy.createController('someWindow').getView().open();

}

The first example creates a reference to the controller that will keep the controller form being garbage collected unless index.js follows the second best practice. But even if it would, unless you really need that reference somewhere else in index.js you shouldn’t create it.

2. Export a cleanup() method on each controller

Other references that can keep a controller from being garbage collected are listeners to global collections created by Alloy (scroll down for the warning), listeners to other global objects or the controller itself and references to and from other controllers required or created.

For this I recommend every controller to export a cleanup() method. Even if you’re 100% sure it doesn’t need it; just have it there – including the first 2 calls in the next example – and call it when the window closes or from another controller that requires its (non-Window) view. If you later add data-binding or controller-events you don’t have to worry about them. And if you add listeners to global objects you just need to add them to the already-existing method.

index.js

$.cleanup = function cleanup() {

  // let Alloy clean up listeners to global collections for data-binding
  // always call it since it'll just be empty if there are none
  $.destroy();

  // remove all event listeners on the controller
  $.off();

  // remove any listeners you added to global proxies
  Ti.Gesture.removeEventListener('orientationchange', onOrientationChange);

  // and custom global dispatchers (all at once, via context)
  myDispatcher.off(null, null, $);

  // in turn, let controllers of required views clean up
  $.requiredView.cleanup();

  // and close windows of controllers you created, triggering them to clean up as well
  someWindow.getView().close();

  // this is not needed if someController cleans up well and we have the only reference
  // someController = null;
};

// you could also do this via the view's onClose attribute
$.win.addEventListener('close', $.cleanup);

I hope these 2 practices help you keep make your app more performant and waterproof.

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


Comments

  • Aaron Saunders

    if the window was defined in the scope of the function, wouldn’t it get cleaned-up? I always assumed it would? The way the post is written is kind of ambiguous.

    However there are a couple of other best practices here that we will definitely leverage, thanks for the great information!

  • Rick Blalock

    So I’d caveat this a little – There’s nothing wrong with creating a reference to your controller. It’s only wrong if you keep your references around when you assume they should go away. The same is true with anything. “Don’t create references to your arrays/objects/strings/etc.”….IF those references are global and you think they’ll go away.

    I use a cleanup method in some controllers if needed but if you scope your app correctly you won’t need to do that a lot. Only in special cases.

  • Shawn

    are you saying if I add an eventlistener to the controller like this:

    controller.on(‘event’,handler);

    later even if I make sure all references to the controller is removed:

    controller = null

    the controller will not be garbage collected, unless I call:

    controller.off()

    titanium’s addEventListener doesn’t require this. Is it specifically for backbone’s .on()?

  • Fokke Zandbergen Post author

    Yes, the window will be cleaned up as soon as the scope isn’t referenced to anymore. That’s exactly the point. As long as there are references from outside the controller (global objects, event listeners, callbacks etc) that could still need the scope, it won’t.

  • Fokke Zandbergen Post author

    Yes, nulling the controller won’t remove the reference the controller has to the handler you added before. So you need to call off either with no arguments to remove all listeners or just with the signature of the listeners you need to be removed.

  • Gabriel

    Hello Fokke, thanks for this, are you saying that saying say var mobilebrowser; at the top of the file, and then later saying

    mobilebrowser = Alloy.CreateController(‘mobilebrowser”);

    mobilebrowser.hideIndicator();

    is bad? If so, I don’t know how else I can call my export methods from certain controllers, ie, hiding and showing an indicator from another file over the mobile browser. Thanks for any help.

  • Fokke Zandbergen Post author

    @Gabriel, just saying that if you don’t need a reference at all, then don’t create it. If you don’t need it outside of the scope where you create it in (a function) then declare the var in that function so it is cleaned when that function is done. And if you need it in other functions in the CommonJS module then indeed go ahead and declare it in the CommonJS module scope. As long as you make certain that when the scope you declare it in disappears, you also make sure the reference is no longer tight to anything.

  • Nidal

    Could you give us some example on a gitub repo ?

    Thank you very much

  • Fokke Zandbergen Post author

    @Nidal what other example then the once in the post do you need?

  • NevilleDastur

    When using something like the iOS NavigationWindow can you confirm how you go about calling the $.cleanup() function? Thanks

  • Fokke Zandbergen (@FokkeZB)

    @NevilleDastur, the windows in the navigation window should still get a close-event when they are closed so you can clean up then.

  • NevilleDastur

    Thanks @Fokke. Can I also ask where methods like off() are documented. I couldn’t find anything in http://docs.appcelerator.com/platform/latest/#!/guide/Alloy_Controllers

    One more question. If I use views instead on windows in a stack is there the equivalent of a close event for views?

  • NevilleDastur

    @FokkeZB found the .off() methods. For others they are part of backbone. But from the minimal documentation I can find on them it looks like .off() will only work on backbone events. Can you confirm .off() cleans up the “native” event listeners bound with .addEventListener(…)
    Thanks

  • Fokke Zandbergen (@FokkeZB)

    @NevilleDastur you are right that we don’t provide much documentation on that. I’ll add it to the URL you mentioned.

    Calling .off() on a controller will only remove event listeners on that controller, not on the views in it. You normally don’t need to remove event listeners from a view. They will clean up when you have no more references to the controller and the primary view has been closed or removed from the view hierarchy. You do need to remove event listeners to global events.

  • NevilleDastur

    @FokkeZB thanks. Can I just check again that .off() on a controller will remove event listeners added with .addEventListener(…)

  • NevilleDastur

    @FokkeZB Looked at the updated wiki. Does that mean there is no need to use _.extend($, Backbone.Events);

  • Fokke Zandbergen (@FokkeZB)

    No, it will not. It will only remove listeners to controller events, added via .on(..) or the XML. Not listeners to views, added via .addEventListener(..) or the XML.

  • Fokke Zandbergen (@FokkeZB)

    Indeed, that is already done for you.

  • Shawn

    Sorry, try to do this again (some things not showing up)

    So Alloy [Require onCustomEvent] and [Widget onCustomEvent] can be used to bind event to the controller, right? My understanding is that it’s similar to $.on(”customEvent”). That means, if I have something like this in Alloy markup, I have to call $.off() explicitly when the required view/widget is closed? I saw your “Pull to Refresh” widget that use [Widget onRefresh]. Please let me know if I should call $.off() if I close the window.

  • Fokke Zandbergen Post author

    Yes, using <Require id="myCtrl" onMyEvent="myFn"> in Alloy XML will compile to $.myCtrl.on('myEvent', myFn) and since it only binds to local instance of a controller, there’s no need to clean up.

  • Shawn

    I’m confused. Your reply a while ago: “nulling the controller won’t remove the reference the controller has to the handler you added before. So you need to call off either with no arguments to remove all listeners or just with the signature of the listeners you need to be removed.” So my understanding is that whether it’s a local instance or not, you have to call $.myCtrl.off()?

  • Fokke Zandbergen Post author

    It’s a all a matter of counting references and keeping in mind that in JS a function is also an object, which includes a reference to it’s parent scope. Read https://developer.mozilla.org/nl/docs/Web/JavaScript/Memory_Management for a good overview.

    If A listens to B and B is only referenced by A then when A dies B will die as well so you don’t need to remove the listeners from A to B.

  • Soumya

    Fantastic. Thanks for sharing such great information.