smorgasbork

 
  • Increase font size
  • Default font size
  • Decrease font size

Titanium Mobile - Gotchas

E-mail Print
If you work with Titanium Mobile to do anything reasonbly complex, you're bound to run into a number of issues that will cost you a great deal of time and possibly part of your sanity. I've compiled some of the worst things I've encountered here to save others some of the frustration I've had.

CommonJS inconsistencies

There are some strange inconsistencies in the way CommonJS works on iOS and android.

globals

On iOS, you can access the global namespace from within a CommonJS module. You'll be in for a rude awakening if you start developing on iOS using CommonJS and global variables when you try to run your app on Android. Your globals will not be accessible, leading to some nasty behavior.

CommonJS modules should not have access to globals, so in this case, the Android behavior is correct. Your best bet is to avoid global variables entirely. You can always define a CommonJS module that can maintain a map of global variables in a static variable. Anybody who needs to access those values can just require your module to get to them.

module.exports vs. exports

When you are building an instantiatable class in your CommonJS module, you assign a function to module.exports. When you are building a generic module with associated functions, you add those functions to exports. Appcelerator strongly recommends that you do not mix those techniques within one module. Either add to exports or set module.exports.

Further, there's another mistake you could make when you're new to CommonJS. If you accidentally assign a function to exports instead of module.exports, like this:
function Foo () {}
exports = Foo;

your code will work on iOS, but on Android, it will crash with the RSOD message "Uncaught TypeError: object is not a function"

This is admittedly confusing, because you can apparently do either this:

function Foo () {}
module.exports.Foo = Foo;

or

function Foo () {}
exports.Foo = Foo;

But when it comes to instantiatable object classes, you can only do it this way:

function Foo () {}
module.exports = Foo;

Extending proxy classes

When you use parasitic inheritance to extend a proxy object, don't add methods to your proxy object that start with "get" or "set". On iOS, the proxy object will intercept all calls to such functions, and your code won't execute.

function MyView ()
{
    var _self = Ti.UI.createView ();

    _self.setSomething = function ()
    {
        // this won't work on iOS
    };

    _self.getSomething = function ()
    {
        // this won't work on iOS
    };

    return _self;
}

Instead, you should adopt a standard naming convention for such functions. You could prepend "x" to the function names like this:

function MyView ()
{
    var _self = Ti.UI.createView ();

    _self.xsetSomething = function ()
    {
    };

    _self.xgetSomething = function ()
    {
    };

    return _self;
}

Adding multiple views

This is sort of an undocumented "feature" of iOS. You can call Ti.UI.View.add () and pass a list of views:

var v = Ti.UI.createView ({...});
var btn1 = Ti.UI.createButton ({...});
var btn2 = Ti.UI.createButton ({...});
v.add (btn1, btn2);
I'm not recommending doing this, but if you did it by accident, it would work on iOS. On android, only the first view, btn1 would be added.

Reusing components

As tempting as it would seem to reuse large or complex UI components (like a window full of subviews), don't do it! You may find that it works fine on iOS, but doesn't work on android.

In my experience reusing a window object, after I hit the hardware "back" button on android (causing the OS to close the window), I was unable to open the window again. The app would then become very unstable, usually crashing after 15-20 seconds.

It's interesting to note that in the Master/Detail Application template in Titanium Studio, the android code explicitly avoids reusing the DetailView and the window it's housed in, while the iOS code happily reuses it.

TableView

It is often convenient to add additional properties to TableViewRows so that in your click event listener, you can determine which row was clicked and know how to respond to the user. If you're using the shortcut of object literals to define your rows, like this:
var rows = [
    { title: 'foo', myprop: 'FOO' }, 
    { title: 'bar', myprop: 'BAR' }, 
    { title: 'baz', myprop: 'BAZ' } 
];

var tv = Ti.UI.createTableView ({
    data: rows
});
you will find that on iOS, each row will have a property myprop, which can be used like this:
tv.addEventListener ('click', function (e) {
  alert (e.row.myprop);
});
But on android, your rows won't have this property! You can work around this by explicitly creating TableViewRow objects and using those with your TableView:
var objects = [];
for (var i = 0; i < rows.length; i++)
{
    var o = Ti.UI.createTableViewRow (rows[i]);
    objects.push (o);
}

var tv = Ti.UI.createTableView ({
    data: objects
});
This will work on both platforms; each row will have a myprop property.

ImageView

ImageView on android will stretch an image to fill the ImageView when the height and width are specified; on iOS, it will not stretch; instead, it will be sized to fit the ImageView, preserving the aspect ratio.

var w = Ti.UI.createWindow ({
    title: 'foo',
    backgroundColor: '#fff'
});
var iv = Ti.UI.createImageView ({
    top: 0, left: 0,
    height: 200, width: 200,
    image: 'http://www.google.com/images/srpr/logo3w.png'
});
w.add (iv);
w.open ();

Path issues

I have found that when it comes to require-ing Javascript files or referencing image resources, it's always best to use absolute paths (like "/images/foo.png") instead of relative paths (like "images/foo.png").

There are times when you can leave off that leading "/", and the system will interpret it as relative to the "Resources" directory. But there are some cases where this doesn't work on all platforms (I believe mobile web has problems with it). Better safe than sorry -- always put the leading '/' on file paths.

Another path-related issue is case sensitivity. Unfortunately, the iOS simulator appears to be case insensitive when it resolves paths and filenames. If you inadvertently use the wrong case in your require() call or your ImageView's image property, it may work in the iOS simulator, but fail on the actual device (and on android). Be meticulous about case in file paths.

One more path issue: if you change the case on a file name, you can cause yourself a lot of heartburn. See this thread: http://developer.appcelerator.com/question/116653/filenotfoundexception-on-android---not-building-all-files

Case sensitivity in property names

It is interesting to note that iOS will interpret the property names of Ti.UI.View proxy objects in a case-insensitive fashion. So this works:

Ti.UI.backgroundColor = 'white';
var win = Ti.UI.createWindow();

var btn = Ti.UI.createButton ({
    Title: 'foo'
});

win.add(btn);
win.open();

However, it will not work on Android. So be very careful with the case of your

Interestingly, this does not work on iOS:

var btn = Ti.UI.createButton ({
    TITLE: 'foo'
});

It appears that the case-insensitivity is only on the first character of the property name, which is fortunate, because if you mess up camel casing of a property (which would be easy to do), it won't work in iOS or Android, so you'll see the problem right away.

Property names of regular Javascript objects are case sensitve, as one would expect, so this will not work on iOS or Android:

var foo = {
    property1: 'foo'
};

alert (foo.Property1);

ActivityWindow has no method 'on'

If you get the following RSOD when launching your application on Android:

  • Location: [284,15 ti:/window.js
  • Message: Uncaught TypeError: Object [object ActivityWindow] has no method 'on'
  • Source: this.window.on("open", function(){

It may be due to issues in an "open" or "focus" event listener. In my case, I was trying to get the actual working size of the window (minus tabs, etc.). I found that sometimes when my event listener fired, I got height and width of 0. So I was setting a timeout to call the event listener again after a few hundred milliseconds. Removing that mechanism (and switching to a "postlayout" event listener) made the problem go away.

TabGroup window management

Managing windows in TabGroups is a bit of a black art.

opening/closing

If you're using a TabGroup, you can't just call

win.open ()

You need to open the window via the current tab. Something like this:

tabgroup.getActiveTab().open (win);

Of course, this means you have to store a reference to the tabgroup somewhere. You'll need to build a CommonJS module for that. Something like this:

var _globals = {};

function set (key, value)
{
    _globals[key] = value;
}

function get (key)
{
    return _globals[key];
}

exports.set = set;
exports.get = get;

When you create the TabGroup, store a reference to it:

var Globals = require ('/Globals');
var tabGroup = Ti.UI.createTabGroup();
Globals.set ('tabGroup', tabGroup);

When you need to open a window from one of your TabGroup windows, retrieve the reference to the TabGroup:

var w = Ti.UI.createWindow ({});
var Globals = require ('/Globals');
var tg = Globals.get ('tabGroup');
tg.getActiveTab ().open (w);

When you need to close a window that you've opened via the Tab.open() function, you do it differently on iOS and android. On iOS, you have to call Tab.close(). On Android, you just call the window's close() function.

checking current window

When you open windows via the active tab, the window reference returned by a call to Tab.getWindow() is handled incorrectly on android. The function should always return the root-level window for the tab. On android, a window is opened via Tab.open(), it becomes the Tab's window (a call to Tab.getWindow() will return the new window). Even worse, when the window is closed via the hardware "back" button, the window reference is not reset back to the original window. So a call to Tab.getWindow() now returns a reference to a window that is closed.

On iOS, the Tab.getWindow() call always returns the original window for the tab, as it should.

You can work around this by calling Tab.setWindow() on android whenever you open a window on a tab.

Here is a bug report in JIRA: https://jira.appcelerator.org/browse/TIMOB-9444

navbars

If the navbar is hidden on a tab window, any window that you open via the Tab.open() function will have its navbar hidden unless you explicitly set navBarHidden=false on the new window.

Inconsistency of dimension units

On iOS, dimensions are specified in "density-independent pixels" (dips), which are the same as Apple's "points", but on Android, the default is to use pixels. This can be a bit confusing when interpreting the value of Ti.Platform.displayCaps.platformWidth. For more than you ever wanted to know about these units, see these two documents:

event issues

debouncing

If you use a button to open a new window, it's possible for the button to process more than one click event before the window appears on screen. If the user taps quickly on the button, he can force your event listener to fire more than once, which might open multiple windows!

You'll need to implement something that disables the button until the window is closed. This is a pretty big failing on the part of the UI framework, IMHO. I've never had to manually debounce things like this when using a client-side UI toolkit.

Window open

There are some inconsistencies with events that are worth noting. On Android, the "open" event for a Window does not always fire when the window is in a TabGroup:

var tabGroup = Ti.UI.createTabGroup();

var win = Ti.UI.createWindow({ backgroundColor: '#fff', title: 'Win 1' }); 

win.addEventListener('open', function() { 
    alert('[open1] size: ' + win.size.width + ', ' + win.size.height); 
});
win.addEventListener('focus', function() { 
    alert('[focus1] size: ' + win.size.width + ', ' + win.size.height); 
});

tabGroup.addTab(Ti.UI.createTab({ window: win, title: win.title }));

win.addEventListener('open', function() { 
    alert('[open2] size: ' + win.size.width + ', ' + win.size.height); 
});
win.addEventListener('focus', function() {
    alert('[focus2] size: ' + win.size.width + ', ' + win.size.height); 
});

tabGroup.open();

On iOS, all 4 alerts fire reliably. On Android, the "open1" alert sometimes fires, sometimes doesn't.

Window close

On Android, if you add a "close" event listener to a window during the execution of the "open" event listener, the "close" event listener won't fire.

Bug report here: https://jira.appcelerator.org/browse/TIMOB-9482

Propagation of data events

Events propagate in some surprising ways in Titanium. I completely understand why touch events propagate up the view hierarchy. It makes sense that you might want to catch those events at different levels of the UI views. What was surprising to me is that other events, like the "change" event propagate.

In the example below, we have a Switch inside a View. We set up a listener for the "change" event of the View (Views don't have change events of their own). When the Switch's value is modified, the Switch fires a "change" event. The View re-fires the "change" event.

This seems odd to me -- I get that if I tap a view which is contained by another view, the tap does apply to both views. But a data change of one control doesn't apply to the parent control.

I encountered this problem when I tried to build a composite control made of a number of switches enclosed in a view; I wanted to fire a "change" event for the top-level view whenever a switch value changed. When I set up a listener, I was getting two events for every switch change; the synthetic one that I was firing, and the one that propagated from switch to view.

Ti.UI.backgroundColor = '#ccc';

var w = Ti.UI.createWindow ({
    title: 'foo'
});

var v = Ti.UI.createView ({
    backgroundColor: '#f00',
});

var s = Ti.UI.createSwitch ({
    titleOn: 'woot',
    titleOff: 'click',
    value: false
});

v.add (s);

v.addEventListener ('change', function (e) {
    alert ('change event fired');
});

w.add (v);

w.open ();

getValue() on android

I have had problems calling getValue() on views like the TextField. I get the RSOD: "Uncaught Error: Requires property name as first argument". To avoid this, I access the .value property directly (that doesn't feel quite right, given that there's an accessor function -- seems like if there's a getter, you should use it!)

See the example below. The first three debug lines will be fine. The last two will result in the RSOD.

Ti.UI.backgroundColor = '#ccc';

var w = Ti.UI.createWindow ({
    title: 'foo'
});

var tf = Ti.UI.createTextField ({
    top: 50,
    width: '90%',
    value: 'woot'
});

tf.addEventListener ('change', function (e) {
    Ti.API.debug ('[change event] e.value = ' + e.value);
    Ti.API.debug ('[change event] e.source.value = ' + e.source.value);
    Ti.API.debug ('[change event] tf.value = ' + tf.value);
    Ti.API.debug ('[change event] e.source.getValue() = ' + e.source.getValue());
    Ti.API.debug ('[change event] tf.getValue() = ' + tf.getValue());
});

w.add (tf);

w.open ();

Android emulator

If you time out waiting for the Android emulator to run,

adb kill-server && adb start-server && adb devices

It seems like if you have a physical android device connected to your computer, your app won't launch on the emulator.

Console craziness

Ti Studio has multiple consoles -- one for your javascript, and one used by native modules (e.g. DoubleClick). There's a dropdown button on the console toolbar that will let you switch between them.

Android and image resources

Titanium allows you to provide separate image resources for various resolutions in the Resources/android/images directories (res-notlong-port-ldpi, res-notlong-port-mdpi, etc.).

If you have an image that exists in one of these resolution-specific directories, but it does not exist in the Resources/images directory, your app will be unable to use it. This is in contrast to iOS, where you can put iphone-specific images into Resources/iphone/images, and they can be used without also existing in Resources/images.

Also be sure that you have images for all resolutions. If you don't have foo.jpg in res-long-port-hdpi, and you run your app on a "long" hdpi device in portait mode, you'll get a ResourceNotFound Exception and you'll see a message like this logged:

Drawable resource could not be opened. Are you sure you have the resource for the current device configuration (orientation, screen size, etc.)?

I recommend that you replace the default resolution-specific directories with these four:

  • res-ldpi
  • res-mdpi
  • res-hdpi
  • res-xhdpi

That way, you only have four variations to deal with.

Restart Required

On Android, if a user installs your application and clicks the "open" button on the install dialog, your app will launch, and an alert dialog will pop up, saying "An application restart is required".

The android market app doesn't launch applications the same way as the home screen (Android bug 5277: http://code.google.com/p/android/issues/detail?id=5277). Therefore, Android sees it as two different invocations of the app and you'll have multiple copies of your app running if you open it right after install, press home, and then launch the app again from the home screen.

Titanium's developers built in the restart dialog to guard against this possibility. But to the user, it does look like an error has occurred in your app, and this is the user's first experience with your app. Not good. In Titanium 2.0 you have some tools to deal with it: https://jira.appcelerator.org/browse/TIMOB-4941

UPDATE: on July 26, a change was made to Titanium to implement an even better fix for this issue. You can use this property in your tiapp.xml:

<property name="ti.android.bug2373.finishfalseroot" type="bool">true</property>

From the JIRA ticket:

If an Titanium developer sets ti.android.bug2373.finishfalseroot to true, then:

  • Play Store launches (and other launches coming directly from the installer) will occur without interruptions (no message to restart.)
  • If user then puts the app in the background and later goes back to it via the application menu (the steps which would exhibit 2373 behavior), the newly-created (unwanted) TiLaunchActivity will recognize it is not the task root (isTaskRoot() == false) and finish itself immediately. I.e., it'll just go away before it has a chance to display anything, and meanwhile the application -- with its activity stack still in place -- will have come forward.

The goal here being that with this new option the user shouldn't notice anything.

WebView touch event handling

Within the Javascript running inside a WebView, stick to touch events as opposed to mouse events:

  • touchstart (instead of mousedown)
  • touchend (instead of mouseup)

In a mobile WebKit implementation, the mousedown and mouseup events will fire, but not when you expect them to. They'll both fire at the same time when the user "finishes" the touch. So avoid mousedown and mouseup. I have found that you can use the click event in conjunction with touchstart and touchend, however.

See the "touch action" section of this page: http://www.quirksmode.org/m/table.html

iPhone WebView

The iPhone fires a mouseover and :hover when the user first touches the element. It fires a mouseout and removes the :hover when the user touches another element. Essentially, the element retains the focus until another element is touched, and mouseover/out and :hover are tied to this focus exclusively.

On every touch touchstart, touchend, mousemove, mousedown, mouseup, and click fire.

If you touch the element and keep your finger there, only touchstart fires.

If the user drags his finger across the element, only the touch events fire.

Android WebView

Android does the same as the iPhone, though it also fires focus and blur events before and after the mousemove.

WebView Wonkiness

There's even more fun to be had with WebViews!
  • "tel:" links on <img> tags don't work on iOS; if you need to have an image that is hyperlinked to a "tel:" URL, use a span with a CSS background image instead
  • certain links do not work on android 4 -- they are not clickable; it is unclear exactly what about the links is wrong, but it seems to be related to the length of the text, and possibly whether it is multiple lines; for example, we had a three-line street address that would not fire its click event no matter what we tried; we ended up breaking it into three one-line hyperlinks, and they all worked; a bug has been filed with android
  • zoom is not always under your control on android; it would appear that some phone manufacturers are modifying the core WebView library code to prevent zoom from being disabled; I've tried every combination of viewport settings and I've made liberal use of WebView.enableZoomControls: false; on some Samsung and HTC phones, I can't make the pinch-zoom go away!

Exception handling inconsistencies

Sometimes there are important differences in the way Titanium handles Javascript exceptions on iOS and Android. For some types of errors, you may get nothing more than a console message on iOS (along with the termination of the current function), but on Android, you get a highly visible RSOD.

var w = Ti.UI.createWindow ({
    title: 'foo',
    backgroundColor: '#fff'
});

var btn1 = Ti.UI.createButton ({
    title: 'click me',
    top: 50,
    width: '80%'
});

var data = [
    ['foo', 'Foo'],
    ['bar', 'Bar'],
    ['baz', 'Baz']
];
var currVal = '';

btn1.addEventListener ('click', function (e) {
    var idx = -1;
    currVal = data[idx][0];
    alert ("currVal: " + currVal);  
});

w.add (btn1);

w.open ();

As you can see, you're going to try to reference the -1th element of the data array, and then operate on it as if it were an array. Clearly, this won't work.

On iOS, your event listener will terminate when you try to set currVal. You'll see the message below in the console:

[WARN] Exception in event callback. {
    line = 21;
    message = "'undefined' is not an object (evaluating 'data[idx][0]')";
    name = TypeError;
    sourceId = 253810880;
    sourceURL = "file://localhost/Users/priebe/Library/Application%20Support/iPhone%20Simulator/5.0/Applications/3E85A4E4-8579-43C7-A60E-67F435D9D7CC/Test.app/app.js";
}

If you're in the simulator, you may get a synthetic breakpoint right at the offending line. But the application will continue to run normally otherwise. If you're in a hurry, you may not notice that the breakpoint wasn't one of your own.

On Android, you'll get the RSOD, with the message:

  • Location: [21,21] app.js
  • Message: Uncaught TypeError: Cannot read property '0' of undefined
  • Source: currVal = data[idx][0];

You can choose to continue, in which case the app will function much like the iOS version.

Note that if you move the offending code out of the event handler and into the main code of app.js, iOS will bring up its RSOD. Depending on where you put the bad code, your UI may not initialize. Obviously, this would be certain to catch your eye. The more subtle form of error handling seems to happen when Titanium is calling an event listener.

The bottom line is this: if you are getting the RSOD on Android but didn't see anything on iOS, that doesn't necessarily mean that the code is working on iOS -- if the failure is subtle enough, you may be getting a failure on iOS, too, but you overlooked the console message (and the terminated function call). It's quite likely that the code is broken in both places, which always makes me feel somewhat better;I'd rather have something be consistently broken than broken just on one platform.

Analytics enabled by default

I have no doubt that Titanium analytics are very useful. But if you've got your own analytics package like Google Analytics or Adobe Site Catalyst, Titanium analytics are redundant. And it's just more network traffic, which isn't going to help the responsiveness of your app.

It's too bad that the TiApp Editor doesn't expose analytics in the GUI -- it would have been a lot more obvious that analytics were turned on. My clue was all the console logging from the Analytics module.

To disable it, look in the tiapp.xml file for this line:

<analytics>true</analytics>

Change it to

<analytics>false</analytics>

clean your project, and build it again. You should then see messages in the console like

[INFO][TiApplication(  374)] (main) [1,1] Analytics have been disabled

Installing modules

There are two ways to install modules on the system: globally or as part of a project. I think it makes more sense to install them as part of a project so that you can easily include the module files in your version control system.

The docs are not clear at all about how you go about doing this.

Here's the most robust way I've found: copy the ZIP file to your application's root directory (not the Resources directory, as stated in the docs, but the directory above it). Ti Studio will detect that file and unpack it next time you build. A module ZIP file should be constructed like this:

- modules
    - android
        - ti.foo
            - A.B.C
            - X.Y.Z
    - iphone
        - ti.foo
            - A.B.C
            - X.Y.Z

A.B.C and X.Y.Z are different versions of the module. Your module may contain a single platform or multiple platforms; it may contain multiple versions of the module for any and all of the platforms (you may have version 1.0.1 for android and 1.2.1 for iphone, for instance).

As long as the files are organized in this directory structure, the TiApp editor will make the module available for selection in the modules picker. Note that modules installed as part of the project get a green icon, as opposed to globally installed modules, which get blue icons.

It's important to use the TiApp editor to add the module, since it will add the directives to tiapp.xml to link up the module. You could do this by hand, but who wants to do that?

More gotchas regarding modules:

  • documentation sucks in many cases (case in point, the AdMob module; the android docs refer to functions that don't exist
  • documentation may contain HTML errors, which now pollute your "Problems" tab in Ti Studio. That's fun to clean up.

Map Inconsistencies

On android, setAnnotations() does not work, while it works just fine on iOS. Instead, call removeAllAnnotations() and then addAnnotations().

Also, setRegion() and setLocation() seem to behave inconsistently on android, while both of them do what you would expect on iOS. These two functions are very poorly documented. I'm not even sure what the difference is between them.

On android, you may find cases where setRegion() does not work, but setLocation() does work, and vice-versa. If you're having trouble getting the region to set properly on android, try both functions!

System clock issues

We needed to test some code that was misbehaving shortly after midnight. Rather than wait until midnight, I wanted to force my clock forward to simulate the condition. First thing I found is that there are no Date/Time Settings in the iOS simulator. OK, so I have to set the system clock on my workstation forward (ugh). Once I did that and then tried to roll the clock back, I got nasty errors in Titanium:

'.../MyApp_Prefix.pch' has been modified since the precompiled header was built

and the app won't compile.

I saw some posts online referring to Xcode's DerivedData folder. But I didn't even have that directory (it should have been in ~/Library/Developer/Xcode/DerivedData).

The only fix for me was to change the GUID in tiapp.xml, clean the project, and build it again. I was a little nervous about changing the guid, since I don't know all the ramifications of doing so. However, I found that I was later able to change it back to the original guid, and the project compiled again.

Back to Titanium Mobile: Beyond the prototype

Comments

avatar Al Mamun
very essentials for titanium mobile app development
Name *
Email
Code   
ChronoComments by Joomla Professional Solutions
Submit Comment
Cancel
avatar Matthew
Excellent post thanks - could save a new Ti developer a lot of time.
Name *
Email
Code   
ChronoComments by Joomla Professional Solutions
Submit Comment
Cancel
avatar David Asher
Excellent summary. One point on setRegion() - I've found that setting a region will work consistently if you always state: mapview.region={latitude, ...}. Even setting the region property to an existing object doesn't work, it always needs to be set to a new region object.
Name *
Email
Code   
ChronoComments by Joomla Professional Solutions
Submit Comment
Cancel
avatar woenz
excellent work! another issue i've found is with the android version of httpclient which is very instable (up to ti 3.1.3). Dont have 2 http clients open at the same time and watch out for 404's and/or timeouts as this can crash your android app as well.
Name *
Email
Code   
ChronoComments by Joomla Professional Solutions
Submit Comment
Cancel
Name *
Email
Code   
Submit Comment