Concepts

From WebOS101

Jump to: navigation, search

This article provides an explanation towards how webOS works, and what makes it a unique mobile platform.

  • Note that this article is highly specific to the Mojo framework, and needs significant revision to bring it up to date.

Contents

Setup

The webOS operating system is a highly customized version of linux. Much of the internals of webOS are covered at webOS internals. Here we're going to keep it simple. SDK applications run in a webkit instance, with Google's V8 javascript engine as the javascript interpreter. Palm has extended the window object with a few custom objects, like window.Mojo. Essentially, we could write applications without Mojo, but we'd have to do a lot more javascript that Palm has already done for us, and we wouldn't have access to some features. Also, Mojo provides custom Palm UI elements that we can use in our applications to provide UI consistency for the user. Palm has also set you up with an instance of Prototype.js to work with inside your application.

So Palm's made it easy, you've got all the tools of the web available in your application. Everything from local databases, to a simple 2D drawing API and audio and video, Palm has provided it to you. V8 manages memory for you. Making calls to web API's is as simple as making an AJAX request. From there it's a matter of understanding application structure.

A Note About Paradigm

WebOS has a few design paradigms to understand. First: Every application is also referred to as a stage. Applications have different views, which are referred to as scenes. Scenes form a tree structure, where the parent is whatever initiated the scene (usually, but not always the stage). In the following example, the stage has scenes "main", "about", "howto", and "options".

Stage
->main
->->options
->about
->howto

In this example, notice that main, about and howto are scenes whose parent is the Stage. However, options is a scene whose parent is another scene, main. In webOS applications you can stack scenes and build a scene hierarchy where it makes sense for your application.

When a scene is initialized, we say it is pushed, when it is uninitialized, we say it is popped. If you push one scene inside of another scene, you make that scene a child of the scene it was launched from. We'll get into this a bit more when we discuss it later in this article.

Application Structure

In a basic application (which can be created using the palm-generate command line tool) the file structure is as follows:

/app/ 
->/assistants/ - The application assistants (js files for each scene)
->/views/ - The application views (html files for each scene)
/images/ - You can place your images here
/stylesheets/ - CSS style sheets are placed here
appinfo.json - Contains application info like the application title
framework_config.json - Some framework configuration settings as a JSON document
icon.png - The application icon
index.html - The base application template, a simple html document
sources.json - contains a list of assistant files the application uses

Inside the /assistants/ directory you have the javascript files related to any scenes that are available, as well as the stage assistant. So let's take an example with scenes "main" and "about". Then the /assistants/ folder would contain three files:

main-assistant.js  //The main scene javascript file
about-assistant.js //The about scene javascript file
stage-assistant.js //This is the javascript file for the StageAssistant

Assistant files related to scenes also have corresponding view files. So if in the previous example we have the "main" and "about" scenes, then we also have views corresponding to those assistants in the /views/ directory as follows:

/about/
->about-scene.html //the html file corresponding to the about scene
/main/
->main-scene.html //the html file corresponding to the main scene

Application Launch Cycle

When the application launches, after doing some other fancy stuff, the application initializes the stage-assistant.js file and the index.html file. The first function call whenever any assistant.js file is initialized is the setup method. The stage assistant doesn't contain any UI. As a developer, you will make sure the stage-assistant.js launches the scene that you want to display first. So if the first scene to display was the "main" scene, then your application would launch that scene first from the stage-assistant.js setup method. Here's how you do that, in this example stage-assistant file:

function StageAssistant() {
}
StageAssistant.prototype.setup = function() {
this.controller.pushScene('main'); //this initializes the main-assistant.js and the main scene
};

At the point that your application gets to this set of code, it's a matter of miliseconds (usually) before your application displays. When you call pushScene, the application initializes the main-assistant.js file and the corresponding view file (which is in the /views/ directory). Again, the first function call when the main-assistant.js file initializes is the setup method of the SceneAssistant object. So in this instance, the setup method of the MainAssistant is called first when the scene is initialized.

function MainAssistant() {
}
MainAssistant.prototype.setup = function() {
//This happens first
};
//other methods of the MainAssistant

What we have yet to discuss, are the scene html templates. As mentioned, every scene has a javascript and an html file. Before the setup methods are called, the html from the scene html file is injected into the application's DOM. So one thing to note is that you can reference any html in the respective html file of a scene inside of the assistant's javascript file.

Events in webOS

To handle events in webOS, we register handlers in the setup method of the scene assistant. The downside is that we need to register events to every scene we create, and every DOM element we create. This is the simplest example, and we'll get to more when we get to widgets, but let's say we want to register a Mojo.Event.tap event to the window. So if the user taps anywhere on the screen, it will call a function.

MainAssistant.prototype.setup = function() {
this.handleTap = this.handleTap.bindAsEventListener(this);
this.tapevent = this.controller.listen('window', Mojo.Event.tap, this.handleTap);
};

So in the following code, we register an event, called Mojo.Event.tap, to the window. When the user taps the window object, the handleTap function is called. Let's take a look at the handleTap function:

MainAssistant.prototype.handleTap = function(e) {
console.log("Yay!");
};

When the handleTap function is called, it will print "Yay!" in the console log. This is a brief introduction to events. For a full list of events take a look at Mojo.Event.

Scene Popping and Pushing

I want to go over a more complex example of popping and pushing scenes. In this example, after every push or pop, we'll take a look at the current structure of the application stack. Events are represented by ALL CAPS.

APPLICATION LAUNCH
//Stack:
//Stage
PUSH "main"
//Stack:
//Stage
//->main
POP "main"
//Stack:
//Stage
PUSH "help"
//Stack:
//Stage
//->help
PUSH "about"
//Stack:
//Stage
//->help
//->->about
POP "about"
//Stack:
//Stage
//->help
POP "help"
//Stack:
//Stage
APPLICATION EXIT

Scenes pushed by the Stage are pushed onto the stage as a child. Scenes pushed by a scene are pushed onto the scene as a child.

Handling Application Interruptions

In webOS, applications can be minimized and still run in the background. If you need to stop something in your application when it's minimized and start it again when the application is maximized, webOS gives you two events that you can use to do this. They are the activate and deactivate methods of a scene assistant. So in the scene called "main", the assistant file would have these two functions:

MainAssistant.prototype.active = function() {
//This happens when the application (and the main scene) is maximized again.
};
MainAssistant.prototype.deactivate = function() {
//This occurs when the application is minimized in the main scene.
};

Asynchronicity

Lots of things in webOS happen asynchronously and require the use of callback functions to properly handle the data. For some, this poses problems. For example, new JavaScript programmers might be confused by the fact that, though their call to an asynchronous method is successful, the data or object they are expecting to manipulate has not "finished" yet. This is because when an asynchronous method is used, the rest of the program continues uninterrupted. You can imagine a scenario like this (pseudo-code):

myFunctionToGetDBResults(objectIWantToStoreResultsIn, db);  // asynchronous function
debugOutput(objectIWantToStoreResultsIn.someResult); // oh no! the object property is undefined

Using Callbacks Effectively

Luckily, callbacks enable you to get the data and do your processing once it is available. Here is a simple example of getting the connection status of the device in the stage controller and delaying pushing the first scene until the call to the asynchronous service completes.

Simple Example

function StageAssistant() {
}
 
StageAssistant.prototype.setup = function() {
this.checkConnection(); // if successful, this will go on to push the first scene, if not, the app will die
};
 
StageAssistant.prototype.checkConnection = function() {
var request = new Mojo.Service.Request('palm://com.palm.connectionmanager', {
'method': 'getstatus',
'parameters': {},
'onSuccess': this.pushFirstScene.bind(this),
'onFailure': this.killApp.bind(this)
});
};
 
StageAssistant.prototype.pushFirstScene = function() {
this.controller.pushScene('main'); // assumes main scene exists and is what you want to push
};
 
StageAssistant.prototype.killApp = function() {
// pop an alert explaining how there was a problem and close the app
};

This is pretty straightforward. However, there is a limitation. What if you wanted to use checkConnection again but do something different? The next example shows how to genericize (yes, I believe I made that word up) the callback system to allow you to pick and choose what to do when the success (or failure) handler fires.

Genericized Example

function StageAssistant() {
}
 
StageAssistant.prototype.setup = function() {
this.checkConnection(this.pushFirstScene.bind(this), this.killApp.bind(this)); // if successful, this will go on to push the first scene, if not, the app will die
};
 
StageAssistant.prototype.checkConnection = function(callback, fallback) {
var request = new Mojo.Service.Request('palm://com.palm.connectionmanager', {
'method': 'getstatus',
'parameters': {},
'onSuccess': callback,
'onFailure': fallback
});
};
 
StageAssistant.prototype.pushFirstScene = function() {
this.controller.pushScene('main'); // assumes main scene exists and is what you want to push
};
 
StageAssistant.prototype.killApp = function() {
// pop an alert explaining how there was a problem and close the app
};

Now we can pass whatever functions we want to use as the callback and fallback methods (note the difference in .setup() and .checkConnection()). These examples are not doing any processing on the response returned by the service because all we are interested in is if the call was successful. Here's an example of looking at the response to see if an Internet connection is available.

Another Example

function StageAssistant() {
}
 
StageAssistant.prototype.setup = function() {
this.checkConnection(this.pushFirstScene.bind(this), this.killApp.bind(this)); // if successful, this will go on to push the first scene, if not, the app will die
};
 
StageAssistant.prototype.checkConnection = function(callback, fallback) {
var request = new Mojo.Service.Request('palm://com.palm.connectionmanager', {
'method': 'getstatus',
'parameters': {},
'onSuccess': callback,
'onFailure': fallback
});
};
 
StageAssistant.prototype.pushFirstScene = function(response) {
if (!response.isInternetConnectionAvailable) { // service call was successful, but there is no Internet connection, fail
this.killApp();
} else {
this.controller.pushScene('main'); // assumes main scene exists and is what you want to push
}
};
 
StageAssistant.prototype.killApp = function() {
// pop an alert explaining how there was a problem and close the app
};

Notice the change in .pushFirstScene() This works because the response handlers for the service request will always receive the response object from the service call.

Finally, here are some abbreviated examples that show other ways to use the callback method described.

Anonymous Function as Callback

MainAssistant.prototype.activate = function(event) {
...
this.controller.stageController.assistant.checkConnection(function(response) { // callback
if (!response.isInternetConnectionAvailable) {
this.controller.get(someDiv).update('Internet is not available');
} else {
this.controller.get(someDiv).update('Internet is available');
}
}.bind(this),
function() { // fallback
Mojo.Log.error('This should not happen since it is a call to a Palm service :D');
}.bind(this));
...
};

Chaining Callbacks

Okay, this one is not-so abbreviated. This example leaves a lot out, but should give a general idea of what you can do.

SomeAssistant.prototype.activate = function(event) {
this.checkStaleness(this.doSomething.bind(this), this.getFreshData.bind(this, this.updateData.bind(this, this.doSomething.bind(this),
function() {
Mojo.Log.error('Update failed');
}.bind(this)),
function() {
Mojo.Log.error('Ajax failed');
}.bind(this)));
// the final callback to run is this.doSomething, the initial fallback is this.getFreshData
// further, this.getFreshData has a callback to this.updateData and an anonymous fallback
// finally, this.updateData has a callback to this.doSomething and an anonymous fallback
};
 
SomeAssistant.prototype.checkStaleness = function(callback, fallback) { // this function would look in a db for a timestamp and if outside a certain range, try to get updated data from elsewhere
theDb.transaction(function(transaction) {
transaction.executeSql(theSql, [the replacement array],
function(transaction, result) {
// check the response for our criteria
if (notStale) { // use this data
if (callback) {
callback(result); // or result.rows.item(blah), or whatever response model you need
}
} else { // try to get better data
if (fallback) {
fallback;
}
}
}.bind(this),
function(transaction, error) {
if (fallback) {
fallback;
} else {
// kill the app? whatever you want to do if you didn't pass a fallback function
}
}.bind(this)
);
}.bind(this));
};
 
SomeAssistant.prototype.getFreshData = function(callback, fallback) { // this function would get the fresh data, presumably an Ajax request
var request = new Ajax.Request(url, { // leaving out some parts here
'onSuccess': callback,
'onFailure': fallback
});
};
 
SomeAssistant.prototype.updateData = function(response, callback, fallback) { // this function should always be called when fresh data is received to update the db
// manipulate response to stick in the db
theDb.transaction(function(transaction) {
transaction.executeSql(theSql, [],
function(transaction, result) {
this.checkStaleness(callback, null); // since we just updated, we shouldn't fail the staleness check, so pass null as the fallback
}.bind(this),
function(transaction, error) {
Mojo.Log.error('Could not update data');
}.bind(this)
);
}.bind(this));
};
 
SomeAssistant.prototype.doSomething = function(response) {
// do something with the data
};
Personal tools