Requirements

Prerequisite knowledge

This article assumes intermediate to advanced knowledge of Flex, ActionScript 3.0, and MXML. You should know how to create Adobe AIR and Flex projects in Flex Builder 3, and be comfortable with a few object-oriented concepts such as inheritance and interfaces, as well as a splash of UML 2.0.

User level

Intermediate

Adobe Flash Player and Adobe AIR provide the ability to run full-featured rich Internet applications (RIAs) either inside a web browser or as native desktop applications on both Windows and Mac OS X, with desktop Linux support coming sometime after the release of Adobe AIR 1.0. The problem for those who want to develop applications that support both desktop and web runtime environments is that Adobe AIR includes extra APIs that allow for local file access, SQLite database support, native windows, and more. These APIs won't compile into a traditional Flex web-based application destined for web-browser distribution (SWF files that you place on a web server and run inside the browser).

In this article, I'm going to show you a technique to set up your Flex Builder 3 workspace and organize your code to output both a Flex web and desktop Adobe AIR application from the same code base. This technique enables you to create a single maintainable code base that will give you web and Adobe AIR applications from your Flex Builder workspace. To demonstrate this, I will set up three projects: one for Adobe AIR, one for the web, and one for the common code. The web and desktop applications will then make calls against an interface that is defined in a common project, but has concrete implementations in each of the desktop and web projects, allowing the desktop version to use the new functionality of Adobe AIR, and giving the developer the option to support a different implementation for the web-based application.

With this knowledge, you'll be able to write software for both a rich browser experience and a desktop application from a code base that doesn't have a bunch of duplicate code. In doing this, you could offer both an online demo with crippled functionality and a desktop version of the application that can run offline with more advanced functionality.

The big picture

As a developer, if you tried to compile a Flex application destined for web-browser distribution that referenced shared code with dependencies on the Adobe AIR libraries, you'd be out of luck with compile-time errors. To get around this, you need to have a specific project for your Flex web output, and another for your Adobe AIR output. On top of this, because you don't want to duplicate all your code in both projects, you need a third project that contains all the common code of the application. Then your common code makes calls to an interface that has abstracted out the functionality that relies on specific runtime functionality (desktop versus web). Don't worry, this sounds a lot worse than it is.

The easiest way to understand where to start is with the big picture of the solution. Look at Figure 1, which is a UML diagram of the sample project. I'll get down to the nitty-gritty of the code in the following sections. It's ok if you don't fully understand everything at this point; it's natural for this to take some time to sink in. Hopefully by the time you're done looking at the code, you'll be a guru of this implementation.

The core application code lives in the Common Project. Adobe AIR specific and Flex specific code live in their own projects.

The main application is MainCanvas.mxml and this needs to be added to both the Adobe AIR Project and Web Project with a call to addChild, just like adding any regular Flex control to a container from within ActionScript. Don't think of your WindowedApplication (for Adobe AIR) or Application ( for Flex Web Project) as your entry point to start coding. These are only shells that host the application.

Next, to call a method that's been extracted, like saveFile, the CommonCode must call against the IGeneral interface that defines the functionality in mind. To get at an implemented version of saveFile, the CommonCode must get a reference to IGeneral from the GeneralFactory object, which retuns objects that implement IGeneral, supporting the saveFile functionality.

GeneralFactory is self-aware of whether it's running inside Adobe AIR or a regular Flex application and will return either FlexGeneralImplementation or AirGeneralImplemenation, depending on the environment it's running in. When compiling the Adobe AIR desktop version of the application, AirGeneralImplementation is compiled into the final application with all its links to the Adobe AIR specific APIs. When compiling the Flex version into a SWF for running inside a web browser, FlexGeneralImplementation (without any references to the Adobe AIR APIs) is compiled into the application.

The GeneralFactory returns the appropriate class to the CommonCode application where the appropriate saveFile method is called. The Adobe AIR Project will use browseForSave, an API specific to Adobe AIR, to save a file locally. The Web Project's implementation just flashes a quick message informing you to download the full version of the application.

Here's how calling abstracted functionality looks in the main application code:

var general:IGeneral; general = GeneralFactory.getGeneralInstance() general.saveFile();

That's it; pretty straightforward after you've organized all the code and created the appropriate classes.

Stepping through the code

Figure 1, on the previous page, shows the code elements you will create in your application to implement the following pattern:

  • The CommonProject project contains elements used by both the AIR and the web project. It contains the MainCanvas.mxml user interface component. This project abstracts any functionality that relies on Adobe AIR specific API support into an interface (IGeneral). This includes functionality such as saving files or writing to SQLite. The GeneralFactory class is a Factory that knows how to create the Adobe AIR and web-specific concrete classes.
  • The AirProject project contains elements specific to the AIR project. It also references the MainCanvas.mxml component, defined in CommonProject.
  • The WebProject project contains elements specific to the AIR project. It too references the MainCanvas.mxml component, defined in CommonProject.

You will implement the specific concrete classes (FlexGeneralImplementation and AirGeneralImplemenation) for both the AIR application and for the browser-based application.

The next section shows you how to create these projects in Flex Builder.

Setting up the Flex Builder 3 workspace and projects

To get started, you'll need to create three projects in a new Flex Builder workspace. I'm assuming you're already familiar with creating projects in Flex Builder. (If not, visit the Flex Developer Center to get started with Flex.)

  1. Create a new Flex Builder workspace.
  2. Create a Flex project named CommonProject. Create this project as a Web application, not as a Desktop (AIR) application. Leave the default settings for the main source folder (src).

    This project is a Flex application that holds the application entry MXML component of your project. In the sample files that accompany this article, it's called CommonProject, and the main MXML component is MainCanvas.mxml. This is the main project where all your application code is going to go for both the web and Adobe AIR applications. This project should never reference any Adobe AIR APIs. This is very important; remember it, live it: No AIR APIs in this project.

  3. From the ZIP source files provided with this article, copy files in the the CommonProject subdirectory into the directory for your CommonProject Flex project.
  4. Create a Flex project named AirProject. Create this as a Desktop (AIR) application. During project creation, you'll be prompted with the ability to add a source path. Click Add Folder and browse to the src subfolder in the folder containing the CommonProject (which you created in step 2). This will share the source code from the common project (which will be important later on). Figure 2 shows what an appropriately added source path looks like. This project will have full access to Adobe AIR APIs.
  1. From the ZIP source files provided with this article, copy files in the the AirProject subdirectory into the directory for your AirProject Flex project.
  2. Create a third project, called WebProject; this should be a regular Flex project that compiles to a simple SWF file for web distribution. Just like the second project above, make sure you add the src subdirectory of the CommonProject project as a Source path (see Figure 2). This project will not have access to the Adobe AIR APIs.
  3. From the ZIP source files provided with this article, copy files in the the WebProject subdirectory into the directory for your WebProject Flex project.

Adding the Common UI component

Since both the Adobe AIR and web-based Flex versions of the application use the same user interface (UI), I'm going to use the MainCanvas.mxml file for all my main UI work. This is the place to start designing your application. In the sample, there's a button (Save File) that triggers the functionality to perform.

In the Adobe Air project's main MXML file, add the following code bound to the applicationComplete event to add the shared MainCanvas.mxml component:

private function onApplicationComplete():void { var can:MainCanvas = new MainCanvas(); this.addChild(can); can.labelMessage = "Loaded in an AIR Application "; }

This adds the Common Project's MainCanvas.mxml UI that was created above to the WindowedApplication entry point of your Adobe AIR application.

For the Web Project, the code is very similar; only the label message changes:

private function onCreationComplete():void { var can:MainCanvas = new MainCanvas(); this.addChild(can); can.labelMessage = "Loaded as Web Browser Flex Application"; }

You'll notice the following lines in the AirProject.mxml and the WebProject.mxml files:

private static const onlyForCompilation:FlexGeneralImplementation = null;

or

private static const onlyForCompilation:AirGeneralImplementation = null;

This declaration of onlyForCompilation is really important. The MXML compiler will remove your definitions of FlexGeneralImplementation and AirGeneralImplementation (described below) since your code doesn't directly reference them (it only references the IGeneral interface ) and creates them dynamically. Without this declaration, when the factory dynamically creates the object, it will fail because it wasn't included in the compiled code. Be warned.

Creating the IGeneral interface

The IGeneral interface defines the functionality that is to be abstracted out. All functionality that calls into specific Adobe AIR APIs need to be defined here. For example, writing to SQLite, implementing native windows, local file I/O, displaying system tray notifications, and so on, will all need to be called through this interface. When it comes to the actual implementation, the classes that implement this interface (FlexGeneralImplementation and AirGeneralImplementation) will do all the hard work.

public interface IGeneral { function saveFile():void; function webOnlyFunctionality():void; function airOnlyFunctionality():void; }

The saveFile mehod is going to be the main method demonstrating the decoupled functionality. The other two methods are added for fun.

Using the factory pattern

To get around compilation errors when compiling a Flex application that references code that relies on Adobe AIR specific APIs, I'm going to rely on a self-aware factory that creates objects that either rely on these Adobe AIR specific APIs or not. The factory design pattern is the key to making all this happen.

At it's most basic level, the factory pattern is an object-oriented creational pattern that creates objects without the client knowing about the specific class that is being created. In the case of this featured solution, the factory is aware of whether it's running inside of Adobe AIR or Flex, and it'll return appropriate classes that have specific Adobe AIR or Flex functionality which implement interfaces that our application understands.

The factory is responsible for creating objects. In this example, the factory GeneralFactory is going to create either Adobe AIR specific or Flex specific objects, based on whether it's running inside a web browser or as a desktop application. It'll create FlexGeneralImplementation when running as a Flex application inside a web browser, or an AirGeneralImplementation when running on the desktop.

GeneralFactory is found in the CommonProject:

  1. Import the IGeneral interface, where the functionality is defined to provide either Adobe AIR or web-specific implementations:
import IGeneral; import flash.system.ApplicationDomain; import flash.system.Security;
  1. Define the GeneralFactory class:
public class GeneralFactory { public static const NOT_SUPPORTED_IN_WEB_MESSAGE:String = "Not supported in web version.";
  1. Create the getClassToCreate which dynamically inspects the current ApplicationDomain. This grabs the definition of the class to create, which is either the AirGeneralImplementation or the FlexGeneralImplementation, and returns the class so it can be created.
static private function getClassToCreate(className:String):Object { var someClass:Object = null; someClass = ApplicationDomain.currentDomain.getDefinition(className); return someClass; }
  1. Expose the generator method (getGeneralInstance). This instantiates an object based on the class definition that was retrieved above in getClassToCreate. The generator is self-aware. It knows whether it's running inside Adobe AIR or regular Flex, and appropriately requests the class to be created. The important thing to note here is how to dynamically create a class at runtime based on a string name.
static public function getGeneralInstance():IGeneral { var general:IGeneral; var cls:String = (isAir ? "AirGeneralImplementation" : "FlexGeneralImplementation"); var clsToCreate:Object = getClassToCreate(cls); general = new clsToCreate(); return general; }
  1. Finally, implement isAir(), which defines the runtime context of the application. Since the constant Security.APPLICATION is only defined in the Adobe AIR framework, use the string "application" instead.
static public function get isAir():Boolean { return Security.sandboxType.toString() == "application" ? true : false; }

Implementing the Adobe AIR and Flex Concrete classes

Above the interface IGeneral was defined. Now it's time to implement the Adobe AIR and Flex project-specific code.

The AirGeneralImplementation class is very simple, yet it demonstrates calling into an Adobe AIR specific API. The browseForSave() brings up an operating system–specific dialog box prompting the user to save a file locally. Calling this API from Flex can't happen—the code won't compile. If you attempted to import flash.filesystem into a Flex project, you'd see a big fat error message: "1172: Definition flash.filesystem could not be found." This is why the function that calls browseForSave was abstracted in this manner, so it will only compile in the Adobe AIR application.

AirGeneralImplementation implements a couple of other methods, like webOnlyFunctionality, which throws an error, since it is not providing an implementation in the Adobe AIR application. The same happens with airOnlyFunctionality, which doesn't have an implementation in the FlexGeneralImplementation.

Since both the FlexGeneralImplementation and AirGeneralImplementation are very similar, I'm only going to show you the code for the Adobe AIR implementation. Keep in mind, that the Flex implementation for saving a file could be anything: it could send it as an e-mail or to the local machine by proxying it through a web server.

The browserForSave is an Adobe AIR API for interacting with native operating system dialog boxes for saving files:

public class AirGeneralImplementation implements IGeneral { public function saveFile():void { var f:File = new File(); f.browseForSave("Set sample to save file as"); } public function webOnlyFunctionality():void { throw new Error("Not Supported."); } public function airOnlyFunctionality():void { Alert.show("This is Adobe AIR Only Functionality"); } }

Calling the implemented functionality

When the code is called, a variable of the IGeneral interface type is defined, and GeneralFactory creates an instance of IGeneral (which it knows how to create based on the runtime environment). Then, simply call the saveFile method and the specific Adobe AIR or Flex implementation of IGeneral (AirGeneralImplementation or FlexGeneralImplementation) will be exeutede. Violà! The Adobe AIR specific functionality was successfully decoupled from the application.

private function saveFile():void { try { var general:IGeneral; general = GeneralFactory.getGeneralInstance() general.saveFile(); } catch (e:Error) { Alert.show("Ooops, something went wrong: " + e.message); } }

Where to go from here

You've learned about dynamic object creation, simple factory pattern implementation, and calling methods on interfaces instead of classes. Seperating your code in this manner offers great design. It decouples functionality that is dependent upon specific Adobe AIR APIs from your main application, allowing you to generate an application for both the web and desktop.

A useful side-effect of implementing your code this way goes beyond just being able to compile both Flex and Adobe AIR applications from a single code base—which was the original goal.

Other benefits include:

  • Decoupled functionality that provides different functionality for web and desktop versions or your application. A desktop version saves the file locally; the online version publishes a file to an online photo storage service, such as Flickr, for example. Or, the desktop version writes data to the local SQLite database and syncs up with online storage when connected; the online version saves directly to online storage.
  • Selective functionality in online version, offering an upgrade path and incentive for users to purchase a full desktop version of the application. This could be useful from a business standpoint (improving cash flow) or from a technical standpoint (like using data from another website that doesn't support cross-domain policy files).
  • Simplified development; implement complex functionality in your Adobe AIR application first, eventually rolling it into your Flex version a few releases later.
  • Minimal code duplication, because you have only one code base.
  • Option to offer an online demo version of the software that users can try without installing any software. Then provide an upgrade path to full-featured desktop application available for purchase. This is value-added proposition that only Adobe supports. No one else can simply offer this in their development platform. This is great for users (no application to install), and great for the company with potential users trying your application out in a matter of seconds.

To learn more about how to adapt a complex codebase to run also as an AIR application, view the presentation Architecting a shared codebase for browser and desktop or download the accompanying slides.

For more information about Adobe AIR, visit the product page.

For more inspiration, check out the sample applications in the Adobe AIR Developer Center for Flex, Flash, and HTML/Ajax.

To get started building Flex apps on AIR go to the Getting Started section of the Adobe AIR Developer Center for Flex or dig into Developing Adobe AIR applications with Adobe Flex. To dive right in and begin building AIR applications in Flex Builder, follow the simple steps in Developing AIR application with Flex on Adobe LiveDocs or explore popular Adobe AIR APIs by working with the AIR Quick Starts.