Titanium

Normalizing Titanium APIs

Generic Feature

When you’re working with cross-platform apps, you soon realize that there are many user interface metaphors that translate across platforms, such as buttons, text fields, alert messages. There are however many others that are very different, not only in the way they are presented to the user but also in the way they are implemented.

For example, iOS has a component called Action Sheet, which slides in from the bottom and is used to present the user with a set of options to choose from.

ActionSheet

On Android, there’s no widget that acts exactly like this, so Appcelerator looks for an equivalent, in this case, is the Android Dialog, which pops up in the center of the screen.

Android Dialog

As you see from the images above, they are both presented to the user in a very similar way: a set of options. What Appcelerator does for you internally is normalize both native functions and wraps them into a single object, in this case, the Ti.UI.OptionDialog. The result is that you don’t have learn about the differences in implementation; Appcelerator takes care of that and gives you a neatly wrapped object.

Now, when the ActionBar was introduced in Android 3.0, Android’s UX started to change. Options can now be part of the ActionBar as icons and drop-down menus. However, the implementation of Action Items is very different from the one of the Android Dialog. Furthermore, there may be times when you want to show your options as a Dialog and not as Action Items; that’s perfectly fine.

normalized

But what if you want to simplify your code in a way that by making a single function call, Appcelerator knows that it should use the ActionSheet on iOS and Action Items on Android like the image above? Well in that case you can write your own normalizations.

Start with what you want to achieve

When you create your own normalizations, you’re the boss, so I suggest you start by thinking about what your ideal implementation should be. First, identify what you’d like to send to your function. In our case we want to be able to:

  • Sending menu options as an array
  • Send a single callback

There are a couple challenges we need to keep in mind:

  • iOS handles the callback of the OptionDialog based on the numerical index of the option that was clicked, while Android requires you to add a callback to each ActionBar Item.
  • iOS has a “destructive” option to cancel the menu
  • The setup of ActionBar Items is done when the Activity is loaded while the OptionDialog can be configured at any time during your Window’s lifecycle.

With these requirements and challenges in mind, you can move on to write your ideal implementation.

var cancelIndex=3;
var OptionsMenu=new TiNorm.OptionsMenu({
    parent:$.index,                                 // so Android knows the Activity to use
    title:"iOS Options Menu",                       // used by iOS
    options:[
        {title:'Option1',icon:"some_icon.png"},     // you can send Android menu properties here
        {title:'Option2',icon:"some_icon.png"},
        {title:'Option3',icon:"some_icon.png"},
        {title:'Cancel',icon:"some_icon.png"}
    ],
    destructive: cancelIndex,                       // index of the cancel button above
    callback:function(e){                           // callback to use for all options
        switch (e.index){
            case 0:
                alert('Clicked on Option1');
                break;
            case 1:
                alert('Clicked on Option2');
                break;
            case 2:
                alert('Clicked on Option3');
                break;
            }
        }
})

Note: You could start building your function first. I like defining the implementation first because it allows me to think about the best way of exposing the functionality, but it’s entirely up to you.

After you have defined your normalized function, it’s time to implement it.

function OptionsMenu(opt){
    this.options=opt;
  if (Ti.Platform.osname==='android'){
    var activity = opt.parent.activity;
    // add menu to ActionBar
    activity.onCreateOptionsMenu = function(e){
      var menu = e.menu;
      opt.options.forEach(function(item,index){
        if (index < opt.options.length-1){
          var menuItem = menu.add({
            title: item.title,
            itemId: index,
            icon: item.icon,
            showAsAction: Ti.Android.SHOW_AS_ACTION_NEVER
          });
          menuItem.addEventListener("click", function(e) {
            // normalize callback so it returns e.index just like iOS
            opt.callback({index:index});
          });
        }
      });
    }
  }
}
OptionsMenu.prototype.show=function(cb){
  _that=this;
  // get only the title property
  var menuOptions=_.pluck(_that.options.options, 'title');
  if (Ti.Platform.osname === 'android'){
    menuOptions.pop(); // remove the destructive
  }
  var opts = {
    title : _that.options.title,
    options : menuOptions,
    destructive : _that.options.destructive
  }
  var dialog = Ti.UI.createOptionDialog(opts);
  dialog.addEventListener('click',_that.options.callback);
  dialog.show();
}
exports.OptionsMenu=OptionsMenu;

When you create a new instance of OptionsMenu and pass all your payload to it, it’ll register the menu options as ActionBar Items on Android. For iOS, it’ll create an OptionsMenu which you can call using the show() method.

Notice that this module effectively allows you to send menu options as an Array and most important of all, normalizes the callback so it behaves in exactly the same way for iOS and Android.

I have uploaded this module to Github and included a sample project. Feel free to use it, fix any bugs or expand on its functionality. Can you think of any other cross-platform object that deserves a user-defined normalization?