Adobe AIR counts already with over 100 million deployments which represents the importance of this development tool nowadays. Nevertheless still some to get introduced to it and start creating their own applications. In this tutorial you will learn how to create an AIR application a bit different on what we have shown before. I mean, we will be showing how to create a RSS Reader with AIR and PureMVC.

As this a very extensive tutorial, due to the several files used in this example, we have opted to use the comments on code to explain it.

Requirements:

Prerequisites

This tutorial is meant for intermediate to advanced developers that already have a reasonable grasp of AS3 and have a small idea about meta-patterns, specifically the MVC meta-pattern.

This isn’t for beginners and designers that are now starting with AS3 or don’t have a reasonable grasp of AS3 yet.

Table of Contents

1 – Goals

2 – Introduction

3 – First Steps

4 – CODE AND COMMENTS

5 – Other frameworks and considerations

6 – Conclusion

1 – Goals

The main goals for this tutorial are to guide you in the development of an AIR application using, and taking advantage of, PureMVC. Also it will try to cover some of the most common changes, and modules, in PureMVC.

2 – Introduction

In this tutorial we will build a simple RSS Reader with AIR using PureMVC.

PureMVC is a framework based on the the Model-View-Controller Meta-Pattern for AS3 (amongst other languages), and although there many others PureMVC seems to be setting itself as the most common choice for medium to large projects due to it’s incredible community, flexibility and utilities ( a concept we won’t discuss here but that you can check over at PureMVC wiki).

As PureMVC comes in many flavors and it’s worth checking them out before you start your project as they have different aims and can greatly ease your development process.

3 – First Steps

After downloading the PureMVC framework and setting up your Flex AIR Project we can copy the PureMVC to the source folder and start coding, but before we dig into the code we’ll go over the package (folder) structure and essential classes of a PureMVC project and what each package means and does.

Default application package (in our case com.techlabs.puremvc)

It should contain the ApplicationFacade.as

The concrete facade acts as a single place for mediators, proxies and commands to access and communicate with each other without having to interact with the Model, View, and Controller classes directly. All this capability it inherits from the PureMVC Facade class. This concrete Facade subclass is also a central place to define notification constants which will be shared among commands, proxies and mediators, as well as initializing the controller with Command to Notification mappings.

Controller (com.techlabs.puremvc.controller)

The purpose of the controller package is to store the command concrete classes and we’ll delve into their types further down.

The concrete commands are meant to be used as actions in the application and often include commands like Application Startup that takes care of initializing the model and views in the PureMVC framework.

Model (com.techlabs.puremvc.model)

The model contains the proxies and further sub-packages that relate to data.

The function of the proxies is to handle all data interactions and to keep that data. Some people prefer to use a command to actually perform the operation and have it affect the proxies, but again this is mostly a question of preference and of the type/size of application.

Business (com.techlabs.puremvc.model.business)

Business usually holds specific service actions that are often referred to as delegates, these take care of making the actual interaction with the webservices and pass the results to the calling proxy, they shouldn’t however do any data handling or parsing.

Helpers (com.techlabs.puremvc.model.helpers)

Helpers are meant for simple classes that perform tasks related to the data that the proxies require such as parsing the xml or doing some data handling. These are useful if several proxies need to do that one same thing ( as parsing xml )

VO (com.techlabs.puremvc.model.vo)

Value Objects are the specific types of data that your application will use, think of them as a class that you make so that you can strong type and cast your data. And they can be anything from a CostumerVo to a BookVo or even a ResourceVo.

View (com.techlabs.puremvc.view)

The view package will hold all the classes that relate to the interface of your application and it’s workings, to that effect it’s common to separate the actual MXML files from their logic using a sub-package called components and using the view package for the mediators.

The mediators will be the gateway between the actual MXML interface and the rest of your application, they manage, listen and interact with all things visual and make sure that all those interactions are sent to the frame work. There are some nuances that we’ll cover further down.

Components (com.techlabs.puremvc.view.components)

The components sub-package holds all the MXML files and visual elements but it does not contain any non-required logic. Ideally it should be a simple description of the user interface.

Note that this is just one way to arrange your classes and most programmers choose to organize it a bit differently to personal taste, so consider this to be a guideline instead of a rule.

4 – Code!

TheTechLabsRssReader.mxml

Let’s initialize the application facade, this is where the framework get’s booted and the fun begins. There different ways of instantiating the facade, like having it on an onInitialize method, or some even more drastic that hack into the Flex initialization process.

<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="facade.startup(this);" xmlns:components="com.techlabs.puremvc.view.components.*" width="800" height="550" verticalScrollPolicy="off" horizontalScrollPolicy="off" viewSourceURL="srcview/index.html"> <mx:Style source="SunNight.css"/> <mx:Script> <![CDATA[ import com.techlabs.puremvc.ApplicationFacade; private var facade:ApplicationFacade = ApplicationFacade.getInstance(); ]]> </mx:Script> <components:MainScreen id="mainScreen"/> <components:AddFeed id="addFeed" x="204" visible="false"/> <mx:Image y="10" source="assets/thetechlabslogo.png" right="10"/> </mx:WindowedApplication>

ApplicationFacade.as

package com.techlabs.puremvc { import com.techlabs.puremvc.controller.AddFeedCommand; import com.techlabs.puremvc.controller.ApplicationStartupCommand; import com.techlabs.puremvc.controller.RemoveFeedCommand; import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.facade.*; import org.puremvc.as3.patterns.proxy.*; /** * */ public class ApplicationFacade extends Facade { // Notification name constants // application public static const STARTUP:String = "startup"; public static const SHUTDOWN:String = "shutdown"; // proxy public static const LOADING_STEP:String = "loadingStep"; public static const LOADING_COMPLETE:String = "loadingComplete"; public static const LOADING_FEED_COMPLETE:String = "loadingFeedComplete"; public static const LOAD_CONFIG_FAILED:String = "loadConfigFailed"; public static const LOAD_RESOURCE_FAILED:String = "loadResourceFailed"; // command public static const COMMAND_STARTUP_MONITOR:String = "StartupMonitorCommand"; public static const FEED_ADDED:String = "feedAdded"; public static const REMOVE_FEED:String = "removeFeed"; // view public static const VIEW_ADD_FEED:String = "viewAddFeed"; public static const VIEW_MAIN_SCREEN:String = "viewMainScreen"; // common messages public static const ERROR_LOAD_FILE:String = "Could Not Load the File!"; /** * Singleton ApplicationFacade Factory Method */ public static function getInstance() : ApplicationFacade { if ( instance == null ) instance = new ApplicationFacade( ); return instance as ApplicationFacade; } /** * Register Commands with the Controller */ override protected function initializeController( ) : void { super.initializeController(); registerCommand( STARTUP, ApplicationStartupCommand ); registerCommand( FEED_ADDED, AddFeedCommand ); registerCommand( REMOVE_FEED, RemoveFeedCommand ); } /** * Start the application */ public function startup( app:TheTechLabsRssReader ):void { sendNotification( STARTUP, app ); } } }

AddFeedCommad.as

package com.techlabs.puremvc.controller { import com.techlabs.puremvc.model.DataProxy; import org.puremvc.as3.interfaces.INotification; import org.puremvc.as3.patterns.command.SimpleCommand; public class AddFeedCommand extends SimpleCommand { /** * Adds a feed to by affecting the feeds in dataProxy */ override public function execute( note:INotification ) :void { var dataProxy:DataProxy = facade.retrieveProxy(DataProxy.NAME) as DataProxy; dataProxy.feeds.addItem(note.getBody()) dataProxy.saveData() } } }

ApplicationStartupCommand.as

package com.techlabs.puremvc.controller { import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.command.*; public class ApplicationStartupCommand extends MacroCommand { /** * Here we have the a MacroCommand that has the ability to combine several Commands * and be called as one, thus maintaining the appropriate order of execution */ override protected function initializeMacroCommand() :void { addSubCommand( ModelPrepCommand ); addSubCommand( ViewPrepCommand ); } } }

ModelPrepCommand.as

package com.techlabs.puremvc.controller { import com.techlabs.puremvc.model.ConfigProxy; import com.techlabs.puremvc.model.DataProxy; import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.command.*; import org.puremvc.as3.patterns.observer.*; public class ModelPrepCommand extends SimpleCommand { /** * This creates all the required proxies, or parts of the model. * In our case we've only two but in a webservice driven application * you'd initialize all your proxies here. */ override public function execute( note:INotification ) :void { facade.registerProxy(new ConfigProxy()); facade.registerProxy(new DataProxy()); } } }

RemoveFeedCommand.as

package com.techlabs.puremvc.controller { import com.techlabs.puremvc.model.DataProxy; import org.puremvc.as3.interfaces.INotification; import org.puremvc.as3.patterns.command.SimpleCommand; public class RemoveFeedCommand extends SimpleCommand { /** * As the name says, this command will affect the dataProxy and remove the selected feed. */ override public function execute(notification:INotification):void { var dataProxy:DataProxy = facade.retrieveProxy(DataProxy.NAME) as DataProxy; dataProxy.feeds.removeItemAt(notification.getBody() as int) dataProxy.saveData(); } } }

ViewPrepCommand.as

package com.techlabs.puremvc.controller { import com.techlabs.puremvc.ApplicationFacade; import com.techlabs.puremvc.view.ApplicationMediator; import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.command.*; import org.puremvc.as3.patterns.observer.*; /** * Here we'll initialize the application mediator that will in hand initialize * the rest of the mediators. */ public class ViewPrepCommand extends SimpleCommand { override public function execute( note:INotification ) :void { // Register the ApplicationMediator facade.registerMediator( new ApplicationMediator( note.getBody() as TheTechLabsRssReader ) ); } } }

ConfigProxy.as

package com.techlabs.puremvc.model { import com.techlabs.puremvc.ApplicationFacade; import com.techlabs.puremvc.model.business.LoadXMLDelegate; import com.techlabs.puremvc.model.helpers.XmlResource; import flash.net.SharedObject; import mx.rpc.IResponder; import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.proxy.Proxy; /** * A proxy for read the config file */ public class ConfigProxy extends Proxy implements IProxy, IResponder { public static const NAME:String = "ConfigProxy"; private var sharedObject:SharedObject; public var mySoData:Object; public function ConfigProxy ( data:Object = null ) { super ( NAME, data ); } public function load():void { // reset the data this.data = new Object(); // create a worker who will go get some data // pass it a reference to this proxy so the delegate knows where to return the data var delegate : LoadXMLDelegate = new LoadXMLDelegate(this, 'assets/config.xml'); // make the delegate do some work delegate.load(); try{ sharedObject = SharedObject.getLocal('rssReader'); mySoData = sharedObject.data }catch(err:Error){ this.sendNotification(ApplicationFacade.ERROR_LOAD_FILE, err.message) } } // this is called when the delegate receives a result from the service public function result( rpcEvent : Object ) : void { // call the helper class for parse the XML data XmlResource.parse(data, rpcEvent.result); } // this is called when the delegate receives a fault from the service public function fault( rpcEvent : Object ) : void { // send the failed notification this.sendNotification( ApplicationFacade.LOAD_CONFIG_FAILED, ApplicationFacade.ERROR_LOAD_FILE ); } /** * Get the config value * * @param key the key to read * @return String the key value stored in internal object */ public function getValue(key:String):String { return data[key.toLowerCase()]; } } }

DataProxy.as

package com.techlabs.puremvc.model

{ import com.techlabs.puremvc.ApplicationFacade; import com.techlabs.puremvc.model.business.LoadXMLDelegate; import com.techlabs.puremvc.model.helpers.SharedObjectActions; import mx.collections.ArrayCollection; import mx.rpc.IResponder; import org.puremvc.as3.interfaces.IProxy; import org.puremvc.as3.patterns.proxy.Proxy; public class DataProxy extends Proxy implements IProxy, IResponder { public static const NAME:String = "DataProxy"; [Bindable] public var feeds:ArrayCollection; public function DataProxy( data:Object=null) { super(NAME, data); //check if a shared object already exists if(SharedObjectActions.getLocalSOData('rssFeeds') == null) feeds = new ArrayCollection(); else feeds = SharedObjectActions.getLocalSOData('rssFeeds') as ArrayCollection; } public function saveData():void { SharedObjectActions.saveToLocalSO('rssFeeds',feeds); } public function loadFeed(url:String):void { var delegate : LoadXMLDelegate = new LoadXMLDelegate(this, url); // make the delegate do some work delegate.load(); } public function result(data:Object):void { //parse the data. var xml:XML = new XML(data.result); var postList:XMLList = xml..item; sendNotification(ApplicationFacade.LOADING_FEED_COMPLETE, postList); } public function fault(info:Object):void { } } }

LoadXMLDelegate.as

package com.techlabs.puremvc.model.business

{ import mx.rpc.AsyncToken; import mx.rpc.events.ResultEvent; import mx.rpc.IResponder; import mx.rpc.http.HTTPService; public class LoadXMLDelegate { private var responder : IResponder; private var service : HTTPService; public function LoadXMLDelegate( responder : IResponder, url:String) { // constructor will store a reference to the service we're going to call this.service = new HTTPService(); this.service.resultFormat = 'xml'; this.service.url = url; // and store a reference to the proxy that created this delegate this.responder = responder; } public function load() : void { // call the service var token:AsyncToken = service.send(); // notify this responder when the service call completes token.addResponder( this.responder ); } } }

SharedObjectActions.as

package com.techlabs.puremvc.model.helpers { import flash.net.SharedObject; public class SharedObjectActions { public static function saveToLocalSO(sharedObjectName:String,data:Object):void { var so:SharedObject = SharedObject.getLocal(sharedObjectName); so.data.data = data; so.flush(); so.close(); } public static function getLocalSOData(sharedObjectName:String):Object { var so:SharedObject = SharedObject.getLocal(sharedObjectName); return so.data.data; } } }

XMLResource.as

package com.techlabs.puremvc.model.helpers { public class XmlResource { static public function parse(data:Object, node:Object, prefix:String=''):void { for(var i:Number=0;i<node.childNodes.length;i++) { var currentNode:Object = node.childNodes[i]; if (currentNode.nodeName=='param' || currentNode.nodeName=='item') { if (currentNode.attributes.value!=null) data[(prefix+currentNode.attributes.name).toLowerCase()] = currentNode.attributes.value; else data[(prefix+currentNode.attributes.name).toLowerCase()] = currentNode.firstChild.nodeValue; } else if (currentNode.nodeName=='group' && currentNode.hasChildNodes()) { XmlResource.parse(data, currentNode, prefix+currentNode.attributes.name+'/'); continue; } if (currentNode.hasChildNodes()) XmlResource.parse(data, currentNode, prefix); } } } }

AddFeedMediator.as

package com.techlabs.puremvc.view { import com.techlabs.puremvc.ApplicationFacade; import com.techlabs.puremvc.view.components.AddFeed; import flash.events.Event; import flash.events.MouseEvent; import org.puremvc.as3.interfaces.IMediator; import org.puremvc.as3.interfaces.INotification; import org.puremvc.as3.patterns.mediator.Mediator; /** * The AddFeedMediator relates to the AddFeed MXML component * and serves to perform all the visual logic and handling * of AddFeed component * * */ public class AddFeedMediator extends Mediator implements IMediator { // Cannonical name of the Mediator public static const NAME:String = "AddFeedMediator"; public function AddFeedMediator(viewComponent:Object=null) { super(NAME, viewComponent); /*There are several reasons why I chose to have an init method instead of just initializing in the constructor, but that's a different topic. */ init() } private function init():void { //here we connect all the event listeners that we require //also we would perform other initialization tasks, like //instantiating variables and such. addFeed.addButton.addEventListener(MouseEvent.CLICK, addAFeed); } /** * List all notifications this Mediator is interested in. * <P> * Automatically called by the framework when the mediator * is registered with the view.</P> * * @return Array the list of Nofitication names */ override public function listNotificationInterests():Array { return [ApplicationFacade.VIEW_ADD_FEED, ApplicationFacade.VIEW_MAIN_SCREEN]; } override public function handleNotification(notification:INotification):void { switch(notification.getName()) { case ApplicationFacade.VIEW_ADD_FEED: addFeed.visible = true; break; case ApplicationFacade.VIEW_MAIN_SCREEN: addFeed.visible = false; break; } } /** * Cast the viewComponent to its actual type. * * <P> * This is a useful idiom for mediators. The * PureMVC Mediator class defines a viewComponent * property of type Object. </P> * * <P> * Here, we cast the generic viewComponent to * its actual type in a protected mode. This * retains encapsulation, while allowing the instance * (and subclassed instance) access to a * strongly typed reference with a meaningful * name.</P> * * @return MainScreen the viewComponent cast to org.puremvc.as3.demos.flex.appskeleton.view.components.MainScreen */ protected function get addFeed():AddFeed { return viewComponent as AddFeed; } /*********************************/ /* events handler */ /*********************************/ private function addAFeed(evt:Event):void { //when we click the add a feed, we should first perform some sort of check //to ensure that the fields are properly filed and then dispatch the appropriate //notifications, in this case we want to call the AddFeedCommand and close the //this component while enabling the main one. sendNotification(ApplicationFacade.FEED_ADDED, {feedName:addFeed.nameInput.text, feedUrl:addFeed.urlInput.text}); sendNotification(ApplicationFacade.VIEW_MAIN_SCREEN); } } }

ApplicationMediator.as

package com.techlabs.puremvc.view { import com.techlabs.puremvc.ApplicationFacade; import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.mediator.Mediator; /** * A Mediator for interacting with the top level Application. * * <P> * In addition to the ordinary responsibilities of a mediator * the MXML application (in this case) built all the view components * and so has a direct reference to them internally. Therefore * we create and register the mediators for each view component * with an associated mediator here.</P> * * <P> * Then, ongoing responsibilities are: * <UL> * <LI>listening for events from the viewComponent (the application)</LI> * <LI>sending notifications on behalf of or as a result of these * events or other notifications.</LI> * <LI>direct manipulating of the viewComponent by method invocation * or property setting</LI> * </UL> */ public class ApplicationMediator extends Mediator implements IMediator { // Cannonical name of the Mediator public static const NAME:String = "ApplicationMediator"; /** * Constructor. * * <P> * On <code>applicationComplete</code> in the <code>Application</code>, * the <code>ApplicationFacade</code> is initialized and the * <code>ApplicationMediator</code> is created and registered.</P> * * <P> * The <code>ApplicationMediator</code> constructor also registers the * Mediators for the view components created by the application.</P> * * @param object the viewComponent (the ApplicationSkeleton instance in this case) */ public function ApplicationMediator( viewComponent:TheTechLabsRssReader ) { // pass the viewComponent to the superclass where // it will be stored in the inherited viewComponent property super( NAME, viewComponent ); // Create and register Mediators // components that were instantiated by the mxml application facade.registerMediator( new MainScreenMediator( app.mainScreen ) ); facade.registerMediator( new AddFeedMediator(app.addFeed) ); } /** * List all notifications this Mediator is interested in. * <P> * Automatically called by the framework when the mediator * is registered with the view.</P> * * @return Array the list of Nofitication names */ override public function listNotificationInterests():Array { return [ ApplicationFacade.VIEW_MAIN_SCREEN ]; } /** * Handle all notifications this Mediator is interested in. * <P> * Called by the framework when a notification is sent that * this mediator expressed an interest in when registered * (see <code>listNotificationInterests</code>.</P> * * @param INotification a notification */ override public function handleNotification( note:INotification ):void { switch ( note.getName() ) { default: break; } } /** * Cast the viewComponent to its actual type. * * <P> * This is a useful idiom for mediators. The * PureMVC Mediator class defines a viewComponent * property of type Object. </P> * * <P> * Here, we cast the generic viewComponent to * its actual type in a protected mode. This * retains encapsulation, while allowing the instance * (and subclassed instance) access to a * strongly typed reference with a meaningful * name.</P> * * @return app the viewComponent cast to AppSkeleton */ protected function get app():TheTechLabsRssReader { return viewComponent as TheTechLabsRssReader } } }

MainScreenMediator.as

package com.techlabs.puremvc.view { import com.techlabs.puremvc.ApplicationFacade; import com.techlabs.puremvc.model.ConfigProxy; import com.techlabs.puremvc.model.DataProxy; import com.techlabs.puremvc.view.components.MainScreen; import flash.events.Event; import flash.events.MouseEvent; import flash.net.URLRequest; import flash.net.navigateToURL; import mx.events.ListEvent; import org.puremvc.as3.interfaces.*; import org.puremvc.as3.patterns.mediator.Mediator; /** * The main screen mediator, is in our case, where most of the action happens * as it's the main screen of our application and you can consider it * to mediate the user interactions with the required data operations. * */ public class MainScreenMediator extends Mediator implements IMediator { // Cannonical name of the Mediator public static const NAME:String = "MainScreenMediator"; private var configProxy:ConfigProxy; private var dataProxy:DataProxy; /** * Constructor. */ public function MainScreenMediator( viewComponent:MainScreen ) { // pass the viewComponent to the superclass where // it will be stored in the inherited viewComponent property super( NAME, viewComponent ); init() } private function init():void { //retrieve the required proxies dataProxy = facade.retrieveProxy(DataProxy.NAME) as DataProxy; configProxy = facade.retrieveProxy(ConfigProxy.NAME) as ConfigProxy; //add the listeners mainScreen.addFeed.addEventListener(MouseEvent.CLICK, addFeed); mainScreen.removeFeed.addEventListener(MouseEvent.CLICK, removeFeed); mainScreen.feedsList.dataProvider = dataProxy.feeds; mainScreen.feedsList.addEventListener(ListEvent.CHANGE, handleFeedListChange); mainScreen.postsList.addEventListener(ListEvent.CHANGE, handlePostListChange); mainScreen.goToPostButton.addEventListener(MouseEvent.CLICK, handleGoToClick); //and disable the gotopost button as an initial state, this could of course also //be set in the mxml, but I think it makes more sense here. mainScreen.goToPostButton.enabled = false; } /** * List all notifications this Mediator is interested in. * <P> * Automatically called by the framework when the mediator * is registered with the view.</P> * * @return Array the list of Nofitication names */ override public function listNotificationInterests():Array { return [ApplicationFacade.VIEW_ADD_FEED, ApplicationFacade.VIEW_MAIN_SCREEN, ApplicationFacade.LOADING_FEED_COMPLETE]; } override public function handleNotification(notification:INotification):void { switch(notification.getName()) { case ApplicationFacade.VIEW_ADD_FEED: mainScreen.enabled = false; break; case ApplicationFacade.VIEW_MAIN_SCREEN: mainScreen.enabled = true; break; case ApplicationFacade.LOADING_FEED_COMPLETE: mainScreen.postsList.dataProvider = notification.getBody(); break; } } /** * Cast the viewComponent to its actual type. * * * This is a useful idiom for mediators. The * PureMVC Mediator class defines a viewComponent * property of type Object. * * * Here, we cast the generic viewComponent to * its actual type in a protected mode. This * retains encapsulation, while allowing the instance * (and subclassed instance) access to a * strongly typed reference with a meaningful * name. * * @return MainScreen the viewComponent cast to org.puremvc.as3.demos.flex.appskeleton.view.components.MainScreen */ protected function get mainScreen():MainScreen { return viewComponent as MainScreen; } /*********************************/ /* events handler */ /*********************************/ private function handlePostListChange(e:ListEvent):void { var tmpXml:XML = e.itemRenderer.data as XML; //this xml hacking is required due to the way that rss feeds are served, //and although the most common approach is to do this server side this also works well //we could also use a RegEx but I chose to simplify the example. var tmpStr:String = tmpXml.toString().replace("content:encoded", "content").replace("content:encoded", "content") tmpXml = new XML(tmpStr) mainScreen.postText.htmlText = tmpXml.content mainScreen.goToPostButton.enabled = true; } private function handleGoToClick(e:MouseEvent):void { var tmpDt:* = mainScreen.postsList.selectedItem.data navigateToURL(new URLRequest(mainScreen.postsList.selectedItem.link)); } private function handleFeedListChange(e:ListEvent):void { //get the feed mainScreen.goToPostButton.enabled = false; dataProxy.loadFeed(e.itemRenderer.data.feedUrl); } private function removeFeed(evt:MouseEvent):void { sendNotification(ApplicationFacade.REMOVE_FEED, mainScreen.feedsList.selectedIndex); } private function addFeed(evt:Event):void { sendNotification(ApplicationFacade.VIEW_ADD_FEED) } } }

AddFeed.mxml

<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="444" height="116" backgroundColor="#212121"> <mx:Label x="192.5" y="10" text="Add a feed"/> <mx:Label x="10" y="37" text="Feed Name:"/> <mx:TextInput x="75" y="37" width="296" id="nameInput"/> <mx:Label x="10" y="67" text="Feed URL:"/> <mx:TextInput x="75" y="67" width="296" id="urlInput"/> <mx:Button x="379" y="37" label="ADD" width="55" height="52" id="addButton"/> </mx:Canvas>

MainScreen.mxml

<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*" width="100%" height="100%" creationComplete="onCreationComplete()"> <mx:Script> <![CDATA[ import com.techlabs.puremvc.model.DataProxy; public static const CREATION_COMPLETE: String = "myCreationComplete"; public function onCreationComplete():void { dispatchEvent( new Event( CREATION_COMPLETE ) ) } ]]> </mx:Script> <mx:Label y="10" text="The TechLabs RSS Reader" horizontalCenter="0" fontSize="15" fontWeight="bold"/> <mx:Label x="70" y="55" text="Feeds"/> <mx:List id="feedsList" x="10" width="160" labelField="feedName" bottom="60" top="82"/> <mx:Button id="addFeed" x="10" label="+" width="96" bottom="10"/> <mx:Button id="removeFeed" x="114" label="-" width="56" bottom="10"/> <mx:List id="postsList" y="82" height="106" labelField="title" left="211" right="10"/> <mx:Label x="452" y="55" text="Posts"/> <mx:HTML id="postText" backgroundAlpha="0" top="196" bottom="60" left="211" right="10"/> <mx:LinkButton x="653" label="Go to Post" id="goToPostButton" bottom="10"/> </mx:Canvas>

5 – Other frameworks and considerations

As a final note I would like to point out that there are several alternatives to Pure MVC and all have their strong and weak points, I’ve written a small article about them at Dreaming In Flash.

Also I would like to point out that Pure MVC is a very dynamic and flexible framework that allows a great deal of customization also that the code in this tutorial is a mere guideline, there are different ways of achieving the same results.

6 – Conclusion

That concludes our tutorial, and I would like to take the time to tell you about some other aspects of Pure MVC and Flex:

You can also get the Pure MVC as a SWC file and just drop it in the libs folder of your Flex project, this has advantages and disadvantages that I will not cover here but that you can find out in the Pure MVC forums, there is also a multitude of different flavors and utilities available at the plugin page.

I sincerely hope that this tutorial will help you get started with Pure MVC and will gladly answer any questions you might have.

I would also like to thank to Scalenine for the skin, and Daniele Ugoletti for his great work on the PureMVC AS3 Demo – Flex Application Skeleton.