Monday 8 March 2010

Appcelerator Titanium and Haxe

I have been tracking the beta development of Appcelerator's Titanium platform over the last year. Today it reaches General Availability. Having spent 300+ hours creating my first Android-powered application in Java, I was intrigued by the prospect of porting Creelcard across to the iPhone without having to learn ObjectiveC. What got me really interested was when I realised that I should be able to use Haxe's cross-platform capabilities to work with Titanium. Because all the source code of a Titanium mobile application is written in Javascript, this opens the door to being able to write iPhone and Android applications in Haxe using the Titanium APIs as extern libraries, compiling out to javascript, and then getting the Titanium SDK to work its magic and build iPhone and Android targets.

I had spent some time experimenting with the earlier versions of the Titanium SDK, but Appcelerator has recently re-designed the APIs in readiness of their v1.0 release. I have spent a short time since understanding the new APIs (without any documentation, but that is supposed to be coming by end of today), and doing some experiments in Haxe, with some success!

Here's a quick summary of my findings:

1. Haxe js target assumes a windows DOM

Unfortunately in this specific use case, the Haxe javascript - target specific code assumes that the javascript interpreter is running within a standard Browser, with access to the HTML Document Object Model. This was actually OK in the earlier versions of Titanium where all the code effectively ran within the context of a HTML page. But this is no longer the case with v0.9/v1.0 of the Titanium Mobile SDK. So far, I have had to make the following modifications to standard Haxe code:

js\Boot.hx:
private static function __trace(v,i : haxe.PosInfos) {
untyped {
var msg = if( i != null ) i.fileName+":"+i.lineNumber+": " else "";
#if jsfl
msg += __string_rec(v,"");
fl.trace(msg);
#else
#if titanium
API.info(v);
#else
msg += __unhtml(__string_rec(v,""))+"
";
var d = document.getElementById("haxe:trace");
if( d == null )
alert("No haxe:trace element defined\n"+msg);
else
d.innerHTML += msg;
#end
#end
}
}

Trace was not working because of the use of the "document" object which doesn't exist in the native context of a Titanium application. So I used a "titanium" Haxe compiler flag to switch to use a call to Titanium API's own logging method (see below).

private static function __clear_trace() {
untyped {
#if jsfl
fl.outputPanel.clear();
#else
#if titanium
// Do nothing here
#else
var d = document.getElementById("haxe:trace");
if( d != null )
d.innerHTML = "";
#end
#end
}
}

Same thing again with circumventing use of the DOM document object.

private static function __init() {
untyped {
#if titanium
js.Lib.isIE = false;
Lib.isOpera = false;
#else
Lib.isIE = (__js__("typeof document!='undefined'") && document.all != null && __js__("typeof window!='undefined'") && window.opera == null );
Lib.isOpera = (__js__("typeof window!='undefined'") && window.opera != null );
#end

And again, the titanium compiler directive switches out the use of document and window DOM objects again.

js\Lib.hx

#if !titanium
public static var document : Document;
public static var window : Window;
#end

then:
static function __init__() untyped {
#if titanium
document = untyped __js__("{}");
window = untyped __js__("{}");
#else
document = untyped __js__("document");
window = untyped __js__("window");
#end

2. Compile Haxe code into "app.js", or separate js files

Titanium apps start with a single javascript file, called 'app.js" by default, that builds the basic UI scaffolding of the application, and forms a global namespace that is shared by other Windows. You can stuff all your code into this single file, or associate different js files with each Titanium Window. So far, I have been using a single js file, which is conveniently created by the Haxe compiler.

3. Titanium APIs and Haxe extern

The core value of Titanium is the access it provides to the native capabilities of the mobile device (Mobile SDK) or desktop (Desktop SDK) via a rich and extensible set of Javascript APIs. These are (or are imminently about to be as I write this) documented on Appcelerator's website. Additionally, Haxe provides an elegant mechanism to import external Javascript namespaces and libraries / APIs into Haxe. I have started building extern class declarations to be able to access the API directly within haxe in a typesafe fashion.

For example:
package titanium.database;

import titanium.database.ResultSet;

/**
* @see http://www.codestrong.com/timobile/api/titanium/Database/DB/
*/
extern class DB
{

/**
* Same as getLastInsertRowId method.
*/
public var lastInsertRowId:Int;
/**
* Same as getRowsAffected method.
*/
public var rowsAffected:Int;
/**
* Close the database. This should be called to prevent resource leaks.
*/
public function close():Void;

/**
* Perform an operation on the database.
* @param sql SQL String
* @param ?args one or more arguments appearing after the sql parameter. Must be integer, float, string, or any data converted to string
* @return
*/
public function execute(sql:String, ?args:Dynamic):ResultSet;
/**
* The row id of the last insert operation.
* @return
*/
public function getLastInsertRowId():Int;
/**
* The number of rows affected by the last operation
* @return
*/
public function getRowsAffected():Int;
/**
* Remove this database from the device. This is a destructive operation.
*/
public function remove():Void;

}

4. haxe.unit.TestRunner.hx

Unfortunately in this case (again!), the haxe.unit.TestRunner class makes some direct calls to a DOM div element "haxe:trace" if compiling to the javascript target. Like the standard js classes above, I had to make a copy of this file in my own project and switch off these calls using the same "titanium" compiler switch.

5. Compile & Build

I have a simple compile and build chain that lets me go from haxe source to a running application on the Apple iPhone Simulator in 2 clicks:
  1. Set up a Titanium Mobile project in the Titanium Developer application. Leave all the default settings in the tiapp.xml file.
  2. Create and start writing the Haxe source code project ( I use FlashDevelop)
  3. Write the build.hxml fileused by the Haxe compiler. You need to compile to the javascript target, and make sure the custom "titanium' compiler directive is set (so that the changes made in the standard haxe files above will work). Here's what my current build.hxml file looks like for my unit test suite:

    -js CreelcardUnit\Resources\app.js
    -main com.creelcard.iphone.test.main.TestAll
    -cp haxe\src
    -cp haxe\lib\titanium\src
    -cp haxe\lib\haxe
    -D titanium
    -D unittest
    -debug

  4. Run the Haxe compiler using the build.hxml file. If things go OK, you should have an newly minted app.js file sitting in the Resources folder of your Titanium project.
  5. Build & test the Titanium application using the Titanium Developer program. Again, if things go OK, it will automatically launch the appropriate emulator (iPhone or Android) and run your application!





3 comments:

  1. Although haXe is currently used primarily by flash people, i think it's near term future will be targeting Javascript, as Javascript reaches a high so will haXe. This is the great strength of haXe, it sits on top of the platform du jour.

    I'm using haXe to target node.js API and googles closure library.

    ReplyDelete
  2. I'd been considering using haXe to target Titanium. I'm glad I'm not the only one - and also that you've travelled that route before me!

    ReplyDelete
  3. You can redirect Haxe trace statements to wherever (i.e. no need to hack other's code):
    http://haxe.org/doc/cross/trace

    ReplyDelete