September 2, 2010

Loading Classes Dynamically in ActionScript 3

Loading classes dynamically in ActionScript 3 is easy, but there are a couple of tricks to keep in mind.

First of all, to load a class dynamically, use the flash.utils.getDefinitionByName function. Be sure to pass in the fully qualified class name, like this:

var MyClass:Class = getDefinitionByName("com.mydomain.package.MyClass") as Class;
var myInstance:MyClass = new MyClass();

Easy enough. But there’s something to watch out for. Class definitions only get compiled into your SWF if they’re declared somewhere in your code. In other words, if MyClass was never declared, the class definition wouldn’t be included in your SWF, and you would get a runtime error when trying to load it dynamically. (Note that this is not the case for classes native to the runtime — only your own custom classes.) Even importing the class isn’t enough to get it included — it must actually be declared.

There are two ways to work around this issue:

  1. Declare the class somewhere in the code.
  2. Use a command line argument to force the class definition to be included.

Below is an example of declaring the class before loading it dynamically:

MyClass;
// or
var mc:MyClass;
var MyClass:Class = getDefinitionByName("com.mydomain.package.MyClass") as Class;
var myInstance:MyClass = new MyClass();

This works fine, but in my opinion, it may defeat the purpose of loading a class dynamically. Usually you want to load classes dynamically because you’re not sure until runtime which class you’re going to want. If you knew which classes you wanted before you loaded them dynamically, you could just instantiate them directly.

The other option is to use the mxmlc -includes compiler argument like this:

-includes=com.mydomain.package.MyClass

Now, the class definition will be compiled into your SWF, and you can load it dynamically anytime you want without ever having to declare it.

To specify compiler arguments in Flash Builder, follow these steps:

  1. Right-click on your project.
  2. Choose “Properties”.
  3. Select “Flex Compiler”.
  4. Enter your arguments in the “Additional compiler arguments” section.

As an aside, you can use the flash.utils.getQualifiedClassName and flash.utils.getQualifiedSuperclassName functions to get the fully qualified class name of an object instance.

3:30 PM Comments (9) Permalink
August 27, 2010

Using Bookmarklets in Adobe AIR

I use a lot of bookmarklets in my browser: Readability, Delicious, Instapaper, Twitter, etc. So I got to wondering the other day if it was possible to use bookmarklets in AIR. As you can see from the video below, the answer is yes:


There are two different techniques for getting bookmarklets to work in AIR. If your bookmarklet doesn’t need to open a new window, it’s as easy as calling eval() on your bookmarklet JavaScript like this:

this.html.htmlLoader.window.eval("bookmarketJavaScriptHere();");

But if your bookmarklet needs to open a new window, things get a little more complicated. For security reasons, it’s not possible to programmatically call window.open() in AIR unless the call is invoked as a result of user interaction. In other words, the only way to open a new browser window in AIR is for the user to choose to do so. That way, remote content can’t open arbitrary windows and try to trick users into interacting with them (or annoy them with ads). A good way to think about it is that AIR has a built-in popup blocker. (For more information, see HTML Security in Adobe AIR.)

In order to get bookmarklets like Twitter and Delicious to work in AIR, therefore, you have to inject HTML into the host page and give the user something to click on. For a good example (and some pretty slick script-bridging between ActionScript and JavaScript, IMHO), check out the onMore() function in Browser.mxml.

All the code for the BookmarkletDemo project is available here, so check it out if you’re curious how it works.

3:35 PM Comments (1) Permalink
August 19, 2010

If you’re getting mysterious IOErrors with URLLoader, URLStream, or Socket, this might be why

MailBrew was the first application I wrote with the new global error handler feature in AIR 2 and Flash Player 10.1. Whenever an unhandled error is thrown, the application opens a utility window with some details and a request to send the information to me so I can get the bug fixed for the next version. I’m a pretty conscientious coder — even in areas where ActionScript 3 doesn’t require you to be — so I wasn’t expecting many reports, but I got several relating to mysterious IOErrors. Unfortunately, errors caught with the global error handler don’t have stack traces, so debugging wasn’t going to be easy. As far as I could tell, I was already catching and registering for IOErrors and IOErrorEvents in all appropriate places, so I initially had no idea what was going on.

Fortunately, Daniel Koestler was finally able to reproduce the error in ADL and get a stack trace. At first, however, it didn’t seem to make any sense. The line throwing the uncaught error was this:

this.urlLoader = new URLLoader();

It took me a minute to figure it out, but now I think I know what’s going on. I believe when the old URLLoader was being destructed (when this.urlLoader already referenced a URLLoader instance), its close() function was implicitly being called. This was usually fine, but in some circumstances, close() can cause a stream error (a type of IOError) which I never had the opportunity to catch. So even though I was registering for IOErrorEvents, and I was calling close() in a try/catch block, it was possible for the close() function to be called in a way that I could not anticipate.

I’m think I’m going to file this as a bug and ask that IOErrors not be thrown in circumstances where close() is called implicitly (in other words, the code that calls close() in the object’s destructor should swallow the exception rather than allow it to propagate), but until it’s fixed, here’s my work-around:

private function start():void
{
    this.dispose();
    this.urlLoader = new URLLoader();
    this.urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
    this.urlLoader.addEventListener(Event.COMPLETE, onComplete);
    this.urlLoader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, onResponseStatus);
    // Go on to use the loader...
}

public function dispose():void
{
    if (this.urlLoader != null)
    {
        try
        {
            this.urlLoader.close();
        }
        catch (e:IOError)
        {
            // No problem. We're getting rid of it, anyway.
        }
        this.urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, onIOError);
        this.urlLoader.removeEventListener(Event.COMPLETE, onComplete);
        this.urlLoader.removeEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS,
                                           onResponseStatus);
        this.urlLoader = null;
    }
}

As you can see, I’m now making sure to call close() explicitly myself before the runtime has the opportunity to call it implicitly whenever I assign a new instance of a URLLoader to a class-level variable. My guess is that there’s a lot of code out there prone to throwing this error, but because the global error handler is so new, most of us just don’t know it yet.

7:58 PM Comments (2) Permalink
August 9, 2010

MailBrew Beta Test

I’m hoping to update MailBrew this week, but I’d love to get a few beta testers first. Below is a list of the changes I made:

  • Added opt-in analytics (using Google Analytics) to help me better understand how people are using the application.
  • Fixed two runtime errors that people were reporting: IOError and a TypeError.
  • Made the summary window selection and its location persistent across sessions (meaning it will reopen and position itself next time you open the app).
  • Added "Check All" to the Dock and System Tray menus (by special request).
  • Fixed one or two other small bugs that made it through my QA process.

If you want to help beta test 0.91, download the AIR file and give it a try. If you find an issue, please either post details here, or email me at my first name dot my last name at adobe dot com.

Thanks!

5:12 PM Comments (3) Permalink
August 5, 2010

Protect Your AIR Applications From Phantom Monitors

If you use multiple monitors on a regular basis, you have probably been in a situation where a window gets stranded on a nonexistent virtual monitor. Sometimes the fix is to restart the application; sometimes the only way to fix it is to reconnect the monitor, retrieve the window, and drag it on to the main monitor before unplugging it again. Lame.

Don’t write AIR applications that aren’t phantom monitor-proof. If you allow secondary windows to be opened, make sure you think about what will happen when these windows end up on monitors that no longer exist.

AIR and the host operating system usually just do the right thing which is to move all the windows over to the main monitor when non-primary monitors go away. This is always true of normal windows (NativeWindowType.NORMAL). Additionally, if you don’t specify window coordinates at all when opening a new window, the operating system is smart enough to make it appear on an actual monitor rather than one that isn’t there anymore. But if you’re working with lightweight or utility windows, and if you’re controlling where they open, this is something you need to watch out for.

I ran into this issue when working on a new version of MailBrew (which I will probably release this week). I got several requests to make the summary window (a little window that shows you how many unread messages you have for each account) reopen when the application starts, and to position it wherever it was last placed. The problem is that the summary window could very easily be placed on a monitor that no longer exists.

In order to prevent falling victim to a phantom monitor, I wrote this very simple function:

private function verifyPosition():void
{
    var screens:Array = Screen.getScreensForRectangle(this.nativeWindow.bounds);
    if (screens.length == 0)
    {
        var mainScreen:Screen = Screen.mainScreen;
        var newPoint:Point = new Point(mainScreen.visibleBounds.x + 2,
                                       mainScreen.visibleBounds.y + 2);
        this.setLocation(newPoint);
    }
}

All I have to do is call verifyPosition from any code that might cause the summary window to be repositioned. If the summary window is off-screen, I simply reposition it in the top left-hand corner and let the user move it him/herself.

A more interesting solution might be to write some heuristics which tried to place the summary window in a similar location to where it was placed on the nonexistent window, but in order to get the new version of MailBrew out the door this week, I went with a simpler approach. Maybe I’ll have a go at this problem in the next version (and share the code).

Note that this does not address every circumstance when the summary window could end up on a phantom monitor. Specifically, it does not address the situation where the user places the summary window on a different monitor, then changes the monitor configuration without restarting the application or reopening the summary window from the main application widow. The reason I don’t have a more encompassing solution is that AIR currently does not have APIs to detect a change in the user’s monitor configuration, so the only bulletproof way to do this would be set a timer to constantly check to see if the summary window is on a phantom monitor. That seemed like overkill to me, so I wrote my code to do the following instead:

  • When the application is started, if the summary window will not be visible, it defaults to the top left-hand corner of the main monitor.
  • When the summary window is opened from the main application window, if its last location no longer exists (because it’s on a phantom monitor), it defaults to the top left-hand corner of the main monitor.
  • Whenever new email messages are found, if the summary window is open on a phantom monitor, it is repositioned in the top left-hand corner.

In other words, the summary window will never be irrevocably lost, and it will always fix itself eventually. In no cases will ever require the user to reconnect a monitor to retrieve it.

If your applications might be susceptible to phantom monitors, I highly encourage you to use a technique like this. In this age of powerful multi-headed graphics cards, inexpensive LCDs, and extensive multi-tasking (requiring multiple monitors for many of us), it’s only a matter of time before a user runs into this and thinks significantly less of your application.

6:06 PM Comments (3) Permalink
June 22, 2010

Adding a Tag to Dreamweaver’s Insert Panel

I usually use Dreamweaver to author blog posts and Adobe Developer Center articles, and since I’m usually writing about code, I frequently need the <code> tag. However, for some reason, the <code> tag isn’t included in Dreamweaver’s insert panel, and as far as I can tell, there’s no way to add it from inside Dreamweaver itself.

But there is a way to add it if you don’t mind working behind the scenes. The instructions below explain how to add the <code> tag to the "Text" menu of Dreamweaver’s Insert panel, but this process can obviously be adapted for any tag or menu you want.

  1. Create an 18×18 gif called Code.gif to use as the menu icon. Make sure to use a transparent background or you’ll end up with a box around it. (I made this icon, but I’m sure most of you can do better.)
  2. Place the icon in "/Applications/Adobe Dreamweaver CS5/Configuration/Objects/Text" (or whatever the corresponding directory is on your machine). If you don’t want your tag in the "Text" menu, place it in a different directory inside the "Objects" directory.
  3. Open the terminal and change into the "/Applications/Adobe Dreamweaver CS5/Configuration/Objects/Text" directory (the directory where you just put your icon file). Copy one of the htm files already there and rename it to Code.htm.
  4. Modify it accordingly. If you have any JavaScript or ActionScript experience, it should be obvious how to modify it to insert a <code> tag rather than what it was originally designed to insert. In this case, be sure to use dom.applyCharacterMarkup("code") rather than dom.setTextFormat("code").
  5. Change into the next highest directory ("/Applications/Adobe Dreamweaver CS5/Configuration/Objects") and modify the insertbar.xml file accordingly. Again, it should be obvious how to insert your new tag just by looking at the contents of this file. Be sure to set the MMString:label attribute to the actual name of the tag as you want it to appear in the insert panel (in this case, "Code").
  6. Restart Dreamweaver, then enjoy the convenience of having your tag readily available.

If you’re thinking that this seems like a lot of work to add such a common tag, I agree, so if you know of a better way of doing it, please post in the comments.

1:12 PM Comments (2) Permalink
June 17, 2010

A Simple Zip Utility for Adobe AIR

I threw together a simple application for zipping and unzipping files in AIR using the FZip project. The application isn’t earth-shattering, but it’s a nice Flex 4 and AIR sample project, and all the source code is available. Screenshot below.

Continue reading…

10:09 AM Comments (3) Permalink
June 11, 2010

MailBrew: An Email Notifier Written for AIR 2

We just release the AIR 2 runtime today (technically 2.0.2), and I’m releasing a new application to go along with it: MailBrew. MailBrew is an email notifier that supports Gmail (regular Gmail, and Google Apps email accounts), and IMAP. You can customize the notification location and alert sound on a per-account bases, and you can add as many accounts as you want.

If you haven’t gotten the new runtime, you can find it here, or you can just install MailBrew, and the new runtime will automatically get downloaded and installed. A screencast of MailBrew is available after the jump.

Continue reading…

7:08 AM Comments (6) Permalink
May 10, 2010

My Presentation on Multi-screen Development

I did a presentation the other day at the DC Flex User Group about how to write one application that will adapt to any screen size. The application is called iReverse, and you can watch the presentation here. The source code for the app can be found at the links below:

10:10 AM Comments (5) Permalink
April 28, 2010

Porting an AIR Application from iPhone to Android

I recently decided to port my AIR Twitter client called TweetCards from iPhone to Android. The process primarily consisted of:

  • Resizing assets to compensate for the larger and higher resolution screen.
  • Changing the orientation of the application from portrait to landscape (to take advantage of the higher resolution screen).
  • Adding keyboard controls to take advantage of the D-pad on the Droid.

The entire process probably took about six hours tops. Below is a video of the end result.

Continue reading…

7:58 PM Comments (4) Permalink