Ten tips for building better Adobe AIR applications

Now that we have launched AIR 2, I figured it would be a good time to look back at all the AIR code I've written over the last few months and pick out some of the best snippets and concepts to share with the community. This article describes ten techniques I've used to improve performance, usability, and security of AIR applications, and to make the development process faster and easier:

  • Keeping memory usage low
  • Reducing CPU usage
  • Storing sensitive data
  • Writing a "headless" application
  • Updating the dock and system tray icons
  • Handling network connectivity changes
  • Creating "debug" and "test" modes
  • Detecting when the user is idle
  • Managing secondary windows
  • Programming for different operating systems

1. Keeping memory usage low

I recently wrote an email notification application called MailBrew. MailBrew monitors Gmail and IMAP accounts, then shows Growl-like notifications and plays alerts when new messages come in. Since it's supposed to keep you informed of new email, it obviously has to run all the time, and since it's always running, it has to be very conservative in terms of memory usage (see Figure 1).

Figure 1. MailBrew consumes some memory while initializing, and uses a little every time it checks mail, but it always goes back down.

Since the runtime does automatic garbage collection, as an AIR developer you don't have to manage memory explicitly, but that doesn't mean you are exempt from having to worry about it. In fact, AIR developers should still think very carefully about creating new objects, and especially about keeping references around so that they can't be cleaned up. The following tips will help you keep the memory usage of your AIR applications both low and stable:

  • Always remove event listeners
  • Remember to dispose of your XML
  • Write your own dispose() functions
  • Use SQL databases
  • Profile your applications

Always remove event listeners

You've probably heard this before, but it's worth repeating: when you're done with an object that throws events, remove all your event listeners so that it can be garbage collected.

Here's some (simplified) code from an application I wrote called PluggableSearchCentral that shows adding and removing event listeners properly:

private function onDownloadPlugin():void { var req:URLRequest = new URLRequest(someUrl); var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, onRemotePluginLoaded); loader.addEventListener(IOErrorEvent.IO_ERROR, onRemotePluginIOError); loader.load(req); } private function onRemotePluginIOError(e:IOErrorEvent):void { var loader:URLLoader = e.target as URLLoader; loader.removeEventListener(Event.COMPLETE, onRemotePluginLoaded); loader.removeEventListener(IOErrorEvent.IO_ERROR, onRemotePluginIOError); this.showError("Load Error", "Unable to load plugin: " + e.target, "Unable to load plugin"); } private function onRemotePluginLoaded(e:Event):void { var loader:URLLoader = e.target as URLLoader; loader.removeEventListener(Event.COMPLETE, onRemotePluginLoaded); loader.removeEventListener(IOErrorEvent.IO_ERROR, onRemotePluginIOError); this.parseZipFile(loader.data); }

Another technique is to create a variable that holds an event listener function so that the event listener can easily remove itself, like this:

public function initialize(responder:DatabaseResponder):void { this.aConn = new SQLConnection(); var listener:Function = function(e:SQLEvent):void { aConn.removeEventListener(SQLEvent.OPEN, listener); aConn.removeEventListener(SQLErrorEvent.ERROR, errorListener); var dbe:DatabaseEvent = new DatabaseEvent(DatabaseEvent.RESULT_EVENT); responder.dispatchEvent(dbe); }; var errorListener:Function = function(ee:SQLErrorEvent):void { aConn.removeEventListener(SQLEvent.OPEN, listener); aConn.removeEventListener(SQLErrorEvent.ERROR, errorListener); dbFile.deleteFile(); initialize(responder); }; this.aConn.addEventListener(SQLEvent.OPEN, listener); this.aConn.addEventListener(SQLErrorEvent.ERROR, errorListener); this.aConn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, this.encryptionKey); }

Remember to dispose of your XML

In Flash Player 10.1 and AIR 1.5.2, we added a static function to the System class called disposeXML() which makes sure all the nodes in an XML object are dereferenced and immediately available for garbage collection. If your application parses XML, make sure to call this function when you're finished with an XML object. If you don't use System.disposeXML(), it's possible that your XML object will have circular references which will prevent it from ever being garbage collected.

Below is a simplified version of some code that parses the XML feed generated by Gmail:

var ul:URLLoader = e.target as URLLoader; var response:XML = new XML(ul.data); var unseenEmails:Vector. = new Vector.(); for each (var email:XML in response.PURL::entry) { var emailHeader:EmailHeader = new EmailHeader(); emailHeader.from = email.PURL::author.PURL::name; emailHeader.subject = email.PURL::title; emailHeader.url = email.PURL::link.@href; unseenEmails.push(emailHeader); } var unseenEvent:EmailEvent = new EmailEvent(EmailEvent.UNSEEN_EMAILS); unseenEvent.data = unseenEmails; this.dispatchEvent(unseenEvent); System.disposeXML(response);

Write your own dispose() functions

If you are writing a medium to large application with a lot of classes, it's a good idea to get into the habit of adding "dispose" functions. In fact, you will probably want to create an interface called IDisposable to enforce this practice. The purpose of a dispose() function is to make sure an object isn't holding on to any references that might keep it from being garbage collected. At a minimum, dispose() should set all the class-level variables to null. Wherever there is a piece of code using an IDisposable, it should call its dispose() function when it's finished with it. In most cases, this isn't strictly necessary since these references will usually get garbage collected anyway (assuming there are no bugs in your code), but explicitly setting references to null and explicitly calling the dispose() function has two very important advantages:

  • It forces you to think about how you're allocating memory. If you write a dispose() function for all your classes, you are less likely to inadvertently retain references to instances which could prevent objects from getting cleaned up (which might cause a memory leak).
  • It makes the garbage collector's life easier. If all instances are explicitly nulled out, it's easier and more efficient for the garbage collector to reclaim memory. If your application grows in size at predictable intervals (like MailBrew when it checks for new messages from several different accounts), you might even want to call System.gc() when you're finished with the operation.

Below is a simplified version of some MailBrew code that does explicit memory management:

private function finishCheckingAccount():void { this.disposeEmailService(); this.accountData = null; this.currentAccount = null; this.newUnseenEmails = null; this.oldUnseenEmails = null; System.gc(); } private function disposeEmailService():void { this.emailService.removeEventListener(EmailEvent.AUTHENTICATION_FAILED, onAuthenticationFailed); this.emailService.removeEventListener(EmailEvent.CONNECTION_FAILED, onConnectionFailed); this.emailService.removeEventListener(EmailEvent.UNSEEN_EMAILS, onUnseenEmails); this.emailService.removeEventListener(EmailEvent.PROTOCOL_ERROR, onProtocolError); this.emailService.dispose(); this.emailService = null; }

Use SQL databases

There are several different methods for persisting data in AIR applications:

  • Flat files
  • Local shared objects
  • EncryptedLocalStore
  • Object serialization
  • SQL database

Each of these methods has its own set of advantages and disadvantages (an explanation of which is beyond the scope of this article). One of the advantages of using a SQL database is that it helps to keep your application's memory footprint down, rather than loading a lot of data into memory from flat files. For example, if you store your application's data in a database, you can select only what you need, when you need it, then easily remove the data from memory when you're finished with it.

A good example is an MP3 player application. If you were to store data about all the user's tracks in an XML file, but the user only wanted to look at tracks from a specific artist or of a particular genre, you would probably have all the tracks loaded into memory at once, but only show users a subset of that data. With a SQL database, you can select exactly what the user wants to see extremely quickly and keep your memory usage to a minimum.

Profile your applications

No matter how good you are at memory management or how simple your application is, it's a very good idea to profile it before you release it. An explanation of the Flash Builder profiler is beyond the scope of this article (using profilers is as much an art as a science), but if you're serious about building a well-behaved AIR application, you also have to be serious about profiling it.

Back to top

2. Reducing CPU usage

It's very difficult to provide general tips about CPU usage in AIR applications since the amount of CPU an application uses is extremely specific to the application's functionality, but there is one universal way to reduce CPU usage across all AIR applications: lower your application's frame rate when it's not active.

The Flex framework has frame rate throttling built in. The WindowedApplication class's backgroundFrameRate property indicates the frame rate to use when the application isn't active, so if you're using Flex, simply set this property to something appropriately low like 1.

As I learned when writing MailBrew, sometimes frame rate throttling can be slightly more complicated, however. MailBrew has a notification system that brings up Growl-like notifications when new messages come in (see Figure 2), easing them in with a gradual alpha tween. Of course, these notifications appear even when the application isn't active, and require a decent frame rate in order to fade in and out smoothly. Therefore, I had to turn off the Flex frame rate throttling mechanism and write one of my own.

Figure 2. MailBrew notifications fade in and out, so they need a frame rate of at least 24 even when the application is deactivated.

The technique I used was to specify the application's default frame rate in my ModelLocator class. If you use the Cairngorm Framework, this will look familiar; if you don't, ModelLocator is simply the class which represents the model in an MVC framework. The constant is defined like this:

public static const DEFAULT_FRAME_RATE:uint = 24;

I then listen for the application's activate and deactivate events like this:

this.nativeApplication.addEventListener(Event.ACTIVATE, onApplicationActivate); this.nativeApplication.addEventListener(Event.DEACTIVATE, onApplicationDeactivate);

I also have a bindable variable in my ModelLocator defined like this:

[Bindable] public var frameRate:uint;

The part of my application that manages frame rate changes then listens for changes to the frameRate variable using the ChangeWatcher like this:

ChangeWatcher.watch(ModelLocator.getInstance(), "frameRate", onFrameRateChange);

Now, whenever any part of the code changes the ModelLocator's frameRate variable, the onFrameRateChange function is called:

private function onFrameRateChange(e:PropertyChangeEvent):void { this.stage.frameRate = ml.frameRate; }

Finally, when the application is activated or deactivated, I update the frame rate accordingly, like this:

private function onApplicationActivate(e:Event):void { this.ml.frameRate = ModelLocator.DEFAULT_FRAME_RATE; } private function onApplicationDeactivate(e:Event):void { this.ml.frameRate = 1; }

All this infrastructure allows me to do the following:

  • Change the application's frame rate anywhere in the code just by changing the ModelLocator's frameRate variable.
  • Throttle the frame rate down when the application is inactive (in the background, or when the main application window is closed).
  • Bring the frame rate back up to the value specified by DEFAULT_FRAME_RATE before showing a notification, and then bring it back down after the notification fades.

Writing your own frame rate throttling framework is more complex than using the one built into Flex, but if you need more flexibility (no pun intended), and you still want to keep your application's CPU usage down when it's not active, it's worth the additional time investment.

Back to top

3. Storing sensitive data

As I mentioned above, there are several ways to persist data in an AIR application, each of which has its advantages and disadvantages. But if you want to store data securely, your three best options are:

  • EncryptedLocalStore class
  • Encrypted SQL database
  • Doing your own encryption

If you only need to store a username and password, I would recommend using the EncryptedLocalStore (ELS) class. But if you want to store larger amounts of data, you will likely want to either use an encrypted database (which AIR fully supports), or do the encryption yourself, and save the encrypted data to disk. (I assume that you are using an encrypted database since a tutorial on managing your own encryption is beyond the scope of this article.)

The great thing about using ELS is that you don't need a password or a passphrase to encrypt and decrypt its data which makes your application much more usable. For example, it doesn't do your users any good to store their usernames and passwords for a service if you then have to prompt them for another password or passphrase to decrypt their original credentials. So how can you provide the same good experience that users get with ELS when you have to encrypt more data than ELS can hold?

The answer is to do the following:

  1. Generate a suitably random password.
  2. Store the password using the EncryptedLocalStore.
  3. Use the password to generate a cryptographically secure database key.
  4. Use the resulting key to encrypt and decrypt your database.

This may seem complicated, but fortunately, most of the code you need is already written. Let's take a closer look at each of these steps.

Generating a random password

The code below is a set of functions I wrote to generate very random and un-guessable passwords:

private static const POSSIBLE_CHARS:Array = ["abcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZ","0123456789","~`!@#$%^&*()_-+=[{]}|;:'/"//,<.>/?"]; private function generateStrongPassword(length:uint = 32):String { if (length < 8) length = 8; var pw:String = new String(); var charPos:uint = 0; while (pw.length < length) { var chars:String = POSSIBLE_CHARS[charPos]; var char:String = chars.charAt(this.getRandomWholeNumber(0, chars.length - 1)); var splitPos:uint = this.getRandomWholeNumber(0, pw.length); pw = (pw.substring(0, splitPos) + char + pw.substring(splitPos, pw.length)); charPos = (charPos == 3) ? 0 : charPos + 1; } return pw; } private function getRandomWholeNumber(min:Number, max:Number):Number { return Math.round(((Math.random() * (max - min)) + min)); }

Now that you have a random password, it's time to store it.

Storing your random password

The best way to securely store the password that you will use to generate a database encryption key is to use the EncryptedLocalStore class. The ELS APIs are very simple to use; however, I typically use my as3preferenceslib project, instead. The advantage of using as3preferenceslib is that I can store all my application's preferences using the same APIs. Behind the scenes, as3preferenceslib uses ELS to store data that you designate as secure. The code looks like this:

var ml:ModelLocator = ModelLocator.getInstance(); var prefs:Preference = ml.prefs; var databasePassword:String = prefs.getValue(PreferenceKeys.DATABASE_PASSWORD); if (databasePassword == null) { databasePassword = this.generateStrongPassword(); ml.prefs.setValue(PreferenceKeys.DATABASE_PASSWORD, databasePassword, true); // The third argument indicates secure storage ml.prefs.save(); }

Generating a cryptographically secure database key

Generating a cryptographically secure encryption key is complicated, but fortunately, we have code to do it for you. I use the EncryptionKeyGenerator located in the as3corelib project.

Using the EncryptionKeyGenerator adds another layer of security on top of your encrypted data by associating your random password with your specific user account and your specific machine. In other words, even if someone were to discover your random password, unless they had the user's machine and were logged in as the user, it wouldn't do them any good.

When using the EncryptionKeyGenerator, it's important that you don't store the encryption key that it returns; rather, you store the password that you use to seed it, and generate the encryption key on-demand. The code below demonstrates the correct technique:

// Get the databasePassword from the Preference object using the code shown above, then... var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(databasePassword); // Now use the encryptionKey to encrypt and decrypt your database.

Encrypting and decrypting your database

Now that you have a cryptographically secure database key, all you have to do is pass it in when creating a connection to your database. The sample code below (simplified since it's lacking event listeners) shows loading an encrypted database file and creating a connection to it:

var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(databasePassword); var dbFile:File = File.applicationStorageDirectory.resolvePath("myEncryptedDatabase.db"); var aConn:SQLConnection = new SQLConnection(); aConn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, this.encryptionKey);

Back to top

4. Writing a "headless" application

Applications that continue to run after their main window is closed are sometimes referred to as "headless" applications. Many applications use this paradigm on the Mac, and on Windows systems, it's not unusual for some applications (IM and email clients, for example) to "minimize to the system tray" (see Figure 3).

Figure 3. MailBrew minimized to the system tray on Windows.

AIR applications can be designed to exit when the main application window closes, or they can run as headless applications. The easiest way to make your application continue to run after the main application window closes is to set the autoExit property of NativeApplication to false like this:

private function onApplicationComplete():void { NativeApplication.nativeApplication.autoExit = false; }

If you're using the Flex framework, you can also set this property using the autoExit attribute of the WindowedApplication tag, like this:

Now that you have prevented your application from exiting when the main application window closes, you have to think about reopening the main application window when users need it again—probably when they click the dock or system tray icon. The easiest way to design your application to support this kind of interaction is to put your main application interface in its own component as a child of NativeWindow. The code below shows a cross-platform technique for reopening your main application after it has been hidden or minimized to the system tray:

private function onApplicationComplete():void { NativeApplication.nativeApplication.autoExit = false; if (NativeApplication.supportsDockIcon) { NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, onShowWindow); } else if (NativeApplication.supportsSystemTrayIcon) { SystemTrayIcon(NativeApplication.nativeApplication.icon).addEventListener(ScreenMouseEvent.CLICK, onShowWindow); } } private function onShowWindow(e:Event):void { var mainApplicationUI:MainApplicationUI = new MainApplicationUI(); mainApplicationUI.open(true); }

Another technique for writing a headless application is to stop your main application window from closing and to simply hide it, instead. This technique doesn't require you to set the autoExit property of the NativeWindow to false since you're not actually closing your main application window, but it does require you to write code to stop the window from closing and to set its visilibity property to false, like this:

private function onWindowClosing(e:Event):void { e.preventDefault(); this.visible = false; ml.frameRate = 1; }

The technique for bringing your main application window back is similar to the one described above, but rather than creating a new instance of your main application UI, you simply set your application's NativeWindow.visibility property to true again, like this:

private function onShowWindow(e:Event):void { this.visible = true; ml.frameRate = ModelLocator.DEFAULT_FRAME_RATE; this.nativeWindow.activate(); }

I would say the technique of toggling your main application's visibility property is the easier way to go, and will work in most cases. The only case where it doesn't make sense is if you want users to be able to open multiple instances of your main application UI as in an application like PixelWindow.

Back to top

5. Updating the dock and system tray icons

In order for headless applications to maintain some kind of visual presence, they often take advantage of the dock on Mac, or the system tray on Windows. These icons give users a way to restore the main application window, and they give applications a way to provide end users with information. AIR doesn't provide APIs for superimposing text on top of your application icon, however there are APIs for dynamically generating bitmaps, and for updating your application icons in the dock or the system tray (see Figure 4).

Figure 4. The MailBrew dock icon with the number of unread messages superimposed.

The code below is a sample taken from MailBrew which shows how to add text and graphics on top of of the dock icon so users can see how many unread messages they have at a glance (see Figure 4). A similar technique can be used with the system tray icon, as well, however system tray icons are only 16 pixels square which makes it difficult to superimpose much text on top of them. Windows 7 has support for more expressive task bar icons, which we plan to support with new APIs in the future.

// If we're on Windows, return. This icon wouldn't look very good in the system tray. if (NativeApplication.supportsSystemTrayIcon) return; var unseenCount:uint = getUnreadMessageCount(); // Function for counting the number of unread messages. var unreadCountSprite:Sprite = new Sprite(); unreadCountSprite.width = 128; unreadCountSprite.height = 128; unreadCountSprite.x = 0; unreadCountSprite.y = 0; var padding:uint = 10; // Use FTE APIs to get the best looking text. var fontDesc:FontDescription = new FontDescription("Arial", "bold"); var elementFormat:ElementFormat = new ElementFormat(fontDesc, 30, 0xFFFFFF); var textElement:TextElement = new TextElement(String(unseenCount), elementFormat); var textBlock:TextBlock = new TextBlock(textElement); var textLine:TextLine = textBlock.createTextLine(); textLine.x = (((128 - textLine.textWidth) - padding) + 2); textLine.y = 32; unreadCountSprite.graphics.beginFill(0xE92200); unreadCountSprite.graphics.drawEllipse((((128 - textLine.textWidth) - padding) - 3), 2, textLine.textWidth + padding, textLine.textHeight + padding); unreadCountSprite.graphics.endFill(); unreadCountSprite.addChild(textLine); var shadow:DropShadowFilter = new DropShadowFilter(3, 45, 0, .75); var bevel:BevelFilter = new BevelFilter(1); unreadCountSprite.filters = [shadow, bevel]; var unreadCountData:BitmapData = new BitmapData(128, 128, true, 0x00000000); unreadCountData.draw(unreadCountSprite); // The Dynamic128IconClass referenced below is embedded. var appData:BitmapData = new Dynamic128IconClass().bitmapData; appData.copyPixels(unreadCountData, new Rectangle(0, 0, unreadCountData.width, unreadCountData.height), new Point(0, 0), null, null, true); var appIcon:Bitmap = new Bitmap(appData); // If you do want to change the system tray icon on Windows, as well, add a 16x16 icon to the array below. InteractiveIcon(NativeApplication.nativeApplication.icon).bitmaps = [appIcon];

Back to top

6. Handling network connectivity changes

As more and more data is moved to the cloud, desktop applications that access and cache that data locally become increasingly important. Adobe AIR is the perfect platform for writing these kinds of applications for the following reasons:

  • Great protocol support. Between the runtime itself and third-party ActionScript libraries, you can use any protocol you want for exchanging data between a desktop client and a web service—or easily write your own protocol on top of HTTP or TCP sockets.
  • Multiplatform support. Since the web is inherently crossplatform, if you're going to write a desktop client on top of a web service, it makes sense for it to be crossplatform, as well.
  • Support for web technologies. Since AIR supports web technologies, you can use the same tools and skills to build your desktop client that you use for your web application.

One of the challenges of writing a desktop client on top of a web service is determining network connectivity. Even with the proliferation of WiFi and high-speed wireless data protocols such as 3G (and soon 4G), the truth is that we still are not always connected. Therefore, applications that depend on network connectivity need a way to definitively know whether they're connected or not.

Our first attempt to tackle this problem led us to the NETWORK_CHANGE event on the NativeApplication class. The NETWORK_CHANGE event fires whenever a network connection becomes available or unavailable which we initially thought was sufficient. However, we rapidly realized that this wasn't nearly enough information to let developers know whether or not they can reach a particular service.

For example, network connections come and go as VPN connections are opened or closed, virtual machines are started or stopped, wireless networks come in and out of range, cables are plugged in and unplugged, and so on. And, of course, it's sometimes impossible to predict where an application's services are going to reside; they might be on the public Internet, behind a firewall, or even on the local machine. Finally, even if you can determine for certain that you can reach a service, there's no guarantee that the service is going to be up or capable of responding at the moment you need to access it. All these factors suggested to us that we needed more comprehensive APIs, and some best practices around how to use them.

I've discovered that desktop client applications generally fall into two categories: applications that access a known set of services (Twitter and Facebook clients, for example), and applications that access any number of unpredictable services (RSS aggregators, email or IM clients, and so on). In my experience, it makes sense to handle connectivity changes for these two types of applications differently.

Applications that access a known set of services

If you know which web services your application needs to access, the easiest way to monitor their availability is with the air.net.URLMonitor class (or air.net.SocketMonitor, if you're using TCP rather than HTTP). The URLMonitor class essentially polls a specific set of URLs at a specified interval and lets you know of any status changes as demonstrated in the code below:

private var urlMonitor:URLMonitor; private function onCreationComplete():void { var req:URLRequest = new URLRequest("http://www.myserver.com/myservice"); this.urlMonitor = new URLMonitor(req, [200, 304]); // Acceptable status codes this.urlMonitor.pollInterval = 60 * 1000; // Every minute this.urlMonitor.addEventListener(StatusEvent.STATUS, onStatusChange); this.urlMonitor.start(); } private function onStatusChange(e:Event):void { if (this.urlMonitor.available) { // Everything is fine. } else { // Service is not available. // Consider alerting the user. } }

Applications that access arbitrary services

If you don't know which services your application is going to be accessing because they are configured by the user (as in the case of an email client), or you don't know how many services your application is going to be accessing (as in the case of an RSS aggregator), using the URLMonitor isn't practical. In fact, the less predictable the services that an application accesses are, the more difficult it is to know if the service is actually accessible. For example, even if your application knew that it didn't have a reliable connection to the public Internet, it might still need to aggregate an RSS feed behind your firewall; or even if your application knew there were no network connections available at all, it might still want to access a service running on the local machine. Since network connectivity becomes increasingly difficult to predict and measure, the best technique in many circumstances is to simply try to make the connection, and report an error if it fails.

MailBrew is a great example. Since MailBrew can be configured to access any number of email accounts, there's no reliable way for the application to know if a service is actually reachable until it tries. Therefore, the application handles network errors gracefully by doing the following:

  • Registering for any events that could indicate that something went wrong (IOErrorEvent, HTTP_RESPONSE_STATUS_EVENT, and so on).
  • If a connection problem is discovered, update a flag in the database designating a service as unreachable.
  • Update the UI to communicate to the user that a service isn't available. In Figure 5, my two Gmail accounts are accessible because I have a network connection to the outside world, but my Adobe account is not accessible because I'm not on the VPN.

Note: Even if you use URLMonitor to check the availability of your services, you can't rely on it since it's always possible for a service to become unavailable between the time the monitor last checked and the time your application needs to access it. Therefore, you must always listen for the appropriate events which may indicate a connectivity issue and handle any problems gracefully.

Figure 5. Account names turn red if there's a connection error, and the user can optionally open an error message window.

Network connectivity can be complex, but it's important that applications behave in a way that is informative and intuitive to end users. Using the two techniques described above should enable your application to do the right thing in any circumstance, no matter how unreliable or unpredictable connectivity is.

Back to top

7. Creating "debug" and "test" modes

All application developers know that writing code is a highly iterative process. The workflow is usually to write some code, run the application to test it, and then repeat dozens, hundreds, or even thousands of times, depending on the size of the application.

If your application is accessing external services, however, this process may be complicated by the fact that you could get rate limited (as in the case of a Twitter or an IM client), or accessing a remote service might simply slow the process down significantly (when averaged over hundreds of iterations). There are two techniques I use to managing these kinds of scenarios: test mode and debug mode.

Test mode

When I was writing one of my first mobile AIR applications, TweetCards, I realized very quickly that grabbing data from Twitter on every iteration wasn't going to scale. Not only would it slow down development, but I would probably get rate limited (meaning Twitter would reject requests from me for some period of time because I was connecting too frequently), and I wouldn't be able to work on the application when I didn't have a network connection (I did some of the coding on a cross-country flight).

Figure 6. TweetCards running in "test mode" with fake data.

The answer was to create a test mode which was activated whenever the username and password entered into the credential fields were both "test" (see Figure 6). The code below demonstrates the concept:

private function onSaveAccountInfo(e:MouseEvent = null):void { var username:String = this.usernameInput.value; var password:String = this.passwordInput.value; var ml:ModelLocator = ModelLocator.getInstance(); ml.testMode = (this.usernameInput.value == "test" && this.passwordInput.value == "test"); ml.credentials = {"username":this.usernameInput.value, "password":this.passwordInput.value}; ml.currentScreen = Screen.READ_SCREEN; }

When the application is in test mode, rather than making a request to Twitter for data, I generate my own test data:

private function getTweets():void { var ml:ModelLocator = ModelLocator.getInstance(); if (ml.testMode) { this.createTestData(); } else { this.queryTwitter(); } }

The result is that the data loads instantly, and I'm able to test my application without making any requests to Twitter.

Debug mode

Another technique I discovered while writing MailBrew was to build a "debug" mode into my application. The need for a debug mode arose as soon as I wrote code to check the user's email accounts as soon as the application started up. From then on, every time I wanted to test something in the application—even something as small as the position of a button—I was making requests to several email services which began to slow down my development. The following code was the work-around:

private function onApplicationComplete():void { // If we're running from ADL, put the app in debug mode. ModelLocator.debugMode = Capabilities.isDebugger; }

Inside my InitCommand (the command that runs when the application is initialized), I simply added the following:

if (!ModelLocator.debugMode) new CheckMailEvent().dispatch();

Problem solved. From then on, the CheckMailEvent would not be dispatched when running MailBrew from ADL. And there were some additional advantages, as well. For example, I decided I didn't want to use the new global error handling functionality in AIR 2 when running from ADL, so I turned this line:

this.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);

into this:

if (!ModelLocator.debugMode) this.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);

And finally, there are certain APIs such as Updater.update() and NativeApplication.startAtLogin that will actually throw runtime exceptions when called from ADL since they don't make sense in a development and testing environment. If you use any of these APIs in your application, it's a good idea to wrap them in a conditional which checks for debug mode.

Note: Notice that I use Capabilities.isDebugger in one place to set what is essentially a global debugMode flag rather than using Capabilities.isDebugger in every location where I want to check the mode. The advantage is that I can easily change the criteria that determines whether the application is in debugMode or not in a single location. For example, I might decide to support putting the application into debug mode with a command line argument so the installed version could be put into debug mode for testing purposes.

Back to top

8. Detecting when the user is idle

When we were making sure that it was possible to write a notification system for AIR applications, we realized that we also needed a way to detect if the user is actually at the computer or not so the application would know whether it made sense to actually show the notifications. For example, it wouldn't be very useful if MailBrew continued to show and play notifications if the user wasn't actually there to see and hear them.

The way we solved this problem was with the USER_IDLE and USER_PRESENT events on the NativeApplication class. Registering for these events will tell the application when users have gone idle, and when they've returned to their computers. The code below shows a simple example:

private function onCreationComplete():void { NativeApplication.nativeApplication.idleThreshold = 2 * 60; // In seconds -- the default is 5 minutes. NativeApplication.nativeApplication.addEventListener(Event.USER_IDLE, onUserIdle); NativeApplication.nativeApplication.addEventListener(Event.USER_PRESENT, onUserPresent); } private function onUserIdle(e:Event):void { // No keyboard or mouse input for 2 minutes. } private function onUserPresent(e:Event):void { // The user is back! }

I've found that registering for these events directly in my application isn't all that useful in real-world scenarios; it usually makes more sense for libraries like notification frameworks to do it, instead. For example, the notification framework that MailBrew uses automatically registers for these events, and queues up notifications when the user is idle, then automatically starts showing them when the user returns. Additionally, make sure you take the workflow of your application into account when using these APIs. For example, if your application shows video, you might want to disable the idle timer while media is playing since the user probably isn't actually idle.

For a simple but real-world example of these APIs in action, see my screen saver sample application called SPF, or Screen Protection Factor.

Back to top

9. Managing secondary windows

It's not uncommon for AIR applications to open secondary windows for things like preferences or about boxes. Opening secondary windows in AIR is easy, but there's something you have to watch out for: opening more than one of the same instance. Since AIR doesn't have a concept of modal windows, if you want to prevent users from opening more than one window at a time, you have to program it yourself.

Figure 7. The MailBrew settings window. Only one can be opened at a time.

Fortunately, it's very easy to do. I have a class called WindowManager which contains several functions useful for working with secondary windows such as the following:

/** * Returns an instance of a NativeWindow if it's already open. */ public static function getWindowByTitle(title:String):NativeWindow { var allWindows:Array = NativeApplication.nativeApplication.openedWindows; for each (var win:NativeWindow in allWindows) { if (win.title == title) { return win; } } return null; }

The getWindowByTitle function allows me to open windows like this:

private function onOpenSettings(e:MouseEvent):void { var win:NativeWindow = WindowManager.getWindowByTitle(WindowManager.PREFERENCES); if (win != null) { win.activate(); } else { var prefsWin:PreferencesWindow = new PreferencesWindow(); prefsWin.open(true); } }

This technique ensures that only one Preferences window can be opened at a time—even if the user inadvertently double-clicks the Preferences button. If the Preferences window is already open, it will simply be activated (brought to the front and given focus) rather than duplicated.

As a bonus, here's another useful function out of my WindowManager class:

/** * Hand me a window, and I'll center it on the primary monitor. */ public static function centerWindowOnMainScreen(win:NativeWindow):void { var initialBounds:Rectangle = new Rectangle((Screen.mainScreen.bounds.width / 2 - (win.width/2)), (Screen.mainScreen.bounds.height / 2 - (win.height/2)), win.width, win.height); win.bounds = initialBounds; win.visible = true; }

Back to top

10. Programming for different operating systems

AIR is a crossplatform runtime, but that doesn't mean applications can't or shouldn't recognize the differences between platforms. We have already seen in this article how you have to use different icon sizes when working with the system tray on Windows versus the dock on Mac OS X, but sometimes platform differences go much deeper. For example, in the MailBrew settings window, I have an option on Mac OS X for bouncing the dock icon when new mail arrives which isn't there on Windows, and on Windows, I have an option for flashing the task bar icon which doesn't exist on Mac OS X.

There are several techniques for dealing with these kinds of platform-specific issues, so rather than claiming that one is correct or superior, I'll simply show you a few approaches I experimented with while writing MailBrew. Below is the relevant code for handling platform differences from the PreferencesWindow component:

Do you want the Dock icon to bounce when you get new messages? Do you want the task bar icon to flash when you get new messages?

As you can see, the key here is to use the very convenient Flex states feature to do most of the work. But, of course, there are other ways of doing it, as well. For example, another platform difference in MailBrew is the main application window's tool bar. On Mac OS X, the title and logo are on the left side and the buttons are on the right (see Figure 8).

Figure 8. MailBrew on Windows in the background and Mac OS X in the foreground. Note the different layout of the tool bar.

However, I discovered that on Windows, positioning anything interactive on the right made it too easy to close the window accidentally by inadvertently clicking the window controls. The answer was to reverse the order as demonstrated with the following code:

In this example, rather than using states, I simply rearrange or remove components using their left, right, and visible properties.

Although I try to keep platform differences in my applications to an absolute minimum, the reality is that they do occasionally come up. In my opinion, it's better to write a little code to handle the differences between platforms than to pretend like they don't exist.

Back to top

Where to go from here

I hope this article has given you a few ideas of how you can make your AIR applications better, or how you can make your life as an AIR application developer a little easier. If you're looking for some inspiration or more sample code, just about everything I write gets open sourced and can be found either on Google Code or on GitHub.

你可能感兴趣的:(Ten tips for building better Adobe AIR applications)