Currently there is a growing need, in my opinion, to create dynamic GUIs for Rich Flash applications.
Why would you need different GUIs for the same application? There are few reasons:
- AIR/Flex application - developing an application that will be used as Flex and AIR application, sharing the same code
- Brands - Developing different brands for the same products
- Multi-devices application - developing an application that will be deployed on different devices such as mobile, web and desktop
Let’s talk about multi-device applications. Currently you can develop application for different OS which can be deployed on your desktop, web or mobile device (up until today UMPC and MIDs).
Adobe announced at the GSMA Mobile World Congress that Flash Player 10 will be available on Smartphones running Windows Mobile, Google’s Android, Nokia S60 / Symbian, and Palm. Combine Flash 10 availability on many devices with the release of Flash Catalyst and you can start building applications that will be served on multiple devices. Additionally, Intel announced the optimization and enabling of Adobe Flash Player 10 and Adobe AIR on ARM which creates new possibilities for creating applications using AIR for devices such as mobile phones, set-top boxes, MIDs, TVs, automotive platforms, MP3 players and many others.
Using Flash Catalyst designer can create GUI for the developer which can be served on one device, but what if you need to create the same application for two devices with different GUIs?
One is a mobile device with smaller screen, let’s say the iPhone (which Adobe already has a version of Flash for the iPhone running on emulation software) and another one will be deployed on your home computer browser with a larger screen. You can create a custom Flex component that extends UIComponent and change the sub classes once there is a need for it, however, you may want to build a completely different GUI for each device based on the device capability. Some GUIs may not have the same sub component since you want the GUI to be lightweight to accommodate mobile development.
You can use Flash Catalyst using the State design pattern or use the presentation model (see some of my previous blogs entries). Flash Catalyst allows you to work with the new design/developer separation paradigm and support states but doesn’t allow you to create multi passive views on the same project.
Take a look at the demo project deployed using the Passive Multi-View on a Nokia MID device, UMPC device as an Adobe AIR project, as well as a laptop and desktop deployment:

The solution: I created a new design pattern that is a mix of the Passive View design pattern as well as the Factory design pattern.
With the new design pattern you can create different views. All views are using the same base high level classes, and you can combine them easily with other tools such as: Degrafa, Flex FXG, Flex 3, AS3.0 with CSS.
I used a music service called MP3 Tunes to demonstrate the new design pattern and you can view the complete application here. After you login to your account, you can select the device size and it will show you two different GUIs based on the size you selected. This is just a POC, but in real life application you can use a context class to determine the GUI to be deployed based on user’s screen size or other factors. For instance, you can create a GUI for touch screen applications, or a GUI for Playstation.
To understand the design pattern let’s take a look at the Passive View design pattern and Factory design pattern.
Passive View Design Pattern
The Passive View pattern is a derivative of the Model View Presenter (MVP), which is considered a derivative of the Model-view-controller. The Passive View pattern has some similarities to the “Code Behind” implementation, since both achieve a complete separation of ActionScript logic and MXML or AS component tags. The passive presentation model allows us to easy test our application since we can create test cases just for the logic and if needed test cases for the view
In passive view pattern we split our laundry to two piles, view and presenter:
View
* State is in the view
* View is passive and is not aware of the Presenter
Presenter
* Logic is in the Presenter.
* Presenter observes view events.
* Presenter updates the view data.
* Presenter ‘knows’ about the components in the view.
* Presenter holds the data or point to a data class.
By moving all the logic out of the view, the Passive View pattern can achieve the separation of designer and developer work and create a paradigm where it’s easy to change the view. The view class contains only the components and their states, no events, logic, changes or model. This pattern works great with Flash Catalyst since the designer responsibility is to create the view (pixel) and behavior (state) so you can copy/paste the FXG code into the application and just set the id’s of each component.

Factory Design Pattern
Now throw in the mix the factory design pattern and you can actually create few views for each presenter.
The factory pattern is one of the most basic creational patterns that deals with the problem of creating different products without specifying the exact concrete class that will be created. This is done by by creating a separate method for creating the product abstract class, whose subclasses can override to specify the derived type of product that will be created. The best way to describe that is to think of a pizza restaurant that holds different types of Pizza such as Mushroom pizza, Bacon pizza and other types of pizza, however, they are all pizzas that include the same ingredients such as dough, cheese and others.

Now if we mix these two design patterns together we get the following UML diagram. The Creator uses the factory pattern to find out which view (product) to use and then takes that and pushes it into the presenter. The Sub-presenter and subView can be used by the main view.

Now let’s put it to work and look at the working example with some screen shots. The application creator let you decide which view to use:

Once a view is selected, the presenter is provided with the view and creates the GUI:


The screen shot is of an application I created in Illustrator, converted it to Flash Catalyst and then imported it to Flex.
Let’s take a look at the Factory Class. The class holds two constants for each product and the “AbstractMusicPlayerMain” allows us to be able to select different products that extends that class.
package com.elad.mp3tunes.view
{
import com.elad.mp3tunes.view.mobile.MusicPlayerMain320x480;
import com.elad.mp3tunes.view.web.MusicPlayerMain530x520;
import flash.errors.IllegalOperationError;
public final class MusicPlayerFactory
{
/**
* Music player types enums
*/
public static const WEB:int = 0;
public static const MOBILE:int = 1;
public static function createView(musicPlayerType:Number):AbstractMusicPlayerMain
{
var retVal:AbstractMusicPlayerMain;
switch (musicPlayerType)
{
case WEB:
retVal = new MusicPlayerMain530x520();
break;
case MOBILE:
retVal = new MusicPlayerMain320x480();
break;
throw new IllegalOperationError("The view type " + musicPlayerType + " is not recognized.");
}
return retVal;
}
}
}
The abstract class holds all the components that will be used by presenter, take a look:
package com.elad.musicplayer.view
{
import com.elad.framework.musicplayer.Player;
import mx.components.baseClasses.FxComponent;
import mx.components.baseClasses.FxScrollBar;
import mx.containers.Canvas;
import mx.controls.ProgressBar;
import mx.graphics.graphicsClasses.TextGraphicElement;
public class AbstractMusicPlayer extends Canvas
{
private var player:Player = new Player();
// text
public var songInfoText:TextGraphicElement;
public var currentTimeText:TextGraphicElement;
public var totalTimeText:TextGraphicElement;
// Buttons
public var playButton:FxComponent;
public var pauseButton:FxComponent;
public var forwardButton:FxComponent;
public var rewindButton:FxComponent;
public var randomButton:FxComponent;
public var replyButton:FxComponent;
public var artistsButton:FxComponent;
public var albumsButton:FxComponent;
// sliders
public var songSlider:FxScrollBar;
public var trackProgressBar:ProgressBar;
public var downloadProgressBar:ProgressBar;
public var volumeProgressBar:ProgressBar;
public var volumeSlider:FxScrollBar;
public function AbstractMusicPlayer()
{
super();
}
}
}
The product is each main view mxml class. Keep in mind that the MXML classes allow us to implements interfaces but not to extends a class, the closest way to extend a class is to use the abstract class in the main tag instead of let’s say the Canvas tag. Using that will use the class constructor, which typically is against how abstract is created. Once we repeat the same component they will be overriding the abstract class members;
<view:AbstractMusicPlayer xmlns="http://ns.adobe.com/mxml/2009" xmlns:lib="MusicPlayerSmall_library.*" xmlns:d="http://ns.adobe.com/fxg/2008/dt" xmlns:th="http://ns.adobe.com/thermo/2009" xmlns:ai="http://ns.adobe.com/ai/2008" xmlns:view="com.elad.musicplayer.view.*" backgroundColor="0xe6e6e6" width="320" height="480" horizontalScrollPolicy="off" verticalScrollPolicy="off" borderStyle="solid" borderThickness="3"> <!-- Track Slider --> <Group> <ProgressBar id="downloadProgressBar" left="37" top="84" barSkin="com.elad.musicplayer.view.mobile.components.DownloadProgressBarSkin" trackSkin="com.elad.musicplayer.view.mobile.components.DownloadProgressTrackSkin" minimum="0" maximum="100" labelWidth="0" direction="right" mode="manual" /> <ProgressBar id="trackProgressBar" alpha="0.5" left="37" top="84" barSkin="com.elad.musicplayer.view.mobile.components.TrackProgressBarSkin" trackSkin="com.elad.musicplayer.view.mobile.components.TrackProgressTrackSkin" minimum="0" maximum="100" labelWidth="0" direction="right" mode="manual" /> <FxHScrollBar id="songSlider" left="32" top="72" skinClass="com.elad.musicplayer.view.mobile.components.HorizontalScrollbar1"/> </Group> </view:AbstractMusicPlayer>
The presenter handles the logic of the main application, as well as using sub presenter and sub view if needed.
package com.elad.musicplayer.view.presenter
{
import com.elad.mp3tunes.Music;
import com.elad.mp3tunes.enum.SortType;
import com.elad.mp3tunes.events.AlbumDataEvent;
import com.elad.mp3tunes.events.ArtistsResultEvent;
import com.elad.mp3tunes.events.TrackDataEvent;
import com.elad.mp3tunes.vo.AlbumItemVO;
import com.elad.mp3tunes.vo.AlbumListVO;
import com.elad.mp3tunes.vo.ArtistItemVO;
import com.elad.mp3tunes.vo.ArtistListVO;
import com.elad.mp3tunes.vo.TrackItemVO;
import com.elad.mp3tunes.vo.TrackListVO;
import com.elad.musicplayer.view.AbstractMusicPlayerMain;
import flash.events.Event;
import mx.collections.ArrayCollection;
import mx.controls.Alert;
import mx.events.ListEvent;
/**
* Presentation Pattern - Passive View
*
* @author Elad
*
*/
[Bindable]
public class MusicPlayerMainPresenter
{
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
private var music:Music = Music.getInstance();
private var artistList:ArtistListVO = null;
private var albumList:AlbumListVO = null;
private var trackList:TrackListVO = null;
private var tileResultType:String;
// Corresponding view
private var musicPlayerMain:AbstractMusicPlayerMain;
// Sub-presenters
private var musicPlayerPresenter:MusicPlayerPresenter;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
public function MusicPlayerMainPresenter(musicPlayerMain:AbstractMusicPlayerMain)
{
this.musicPlayerMain = musicPlayerMain;
musicPlayerPresenter = new MusicPlayerPresenter(musicPlayerMain.musicPlayer);
musicPlayerMain.dg.addEventListener(Event.CHANGE, dgChangeHandler);
musicPlayerMain.tileList.addEventListener(Event.CHANGE, selectTileListItem);
musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.NEXT_TRACK_EVENT, nextTrack);
musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.PREVIOUS_TRACK_EVENT, previousTrack);
musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.ARTISTS_CLICK_EVENT, getAllArtists);
musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.ALBUMS_CLICK_EVENT, getAllAlbums);
musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.PLAYING_COMPLETED, function():void { nextTrack(null); } );
getAllArtists();
}
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
private function nextTrack(event:Event):void
{
var trackItem:TrackItemVO = trackList.getItem(++musicPlayerMain.dg.selectedIndex);
musicPlayerPresenter.playSong(trackItem);
}
private function previousTrack(event:Event):void
{
var trackItem:TrackItemVO = trackList.getItem(--musicPlayerMain.dg.selectedIndex);
musicPlayerPresenter.playSong(trackItem);
}
private function getAllAlbums(event:Event=null):void
{
music.addEventListener(AlbumDataEvent.ALL_ALBUMS_DATA_COMPLETED, onAllAlbumCompleted);
music.getAllAlbums(artistList);
}
private function getAllArtists(event:Event=null):void
{
music.addEventListener(ArtistsResultEvent.ARTIST_RESULT_COMPLETED, onArtistsResult);
music.addEventListener(ArtistsResultEvent.ARTIST_RESULT_ERROR, function(event:ArtistsResultEvent):void { Alert.show(String(event.message)); });
music.getMusicByArtists();
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
protected function selectTileListItem(event:ListEvent):void
{
var index:Number = event.columnIndex;
getTrackList(index);
}
protected function getTrackList(index:Number):void
{
var id:String;
music.addEventListener(TrackDataEvent.TRACK_DATA_COMPLETED, onTrackDataComplete);
music.addEventListener(TrackDataEvent.TRACK_DATA_ERROR, function():void { Alert.show("Error getting track data"); });
// based on the type in the tile list
switch (tileResultType)
{
case SortType.ALBUMS:
var albumItem:AlbumItemVO = albumList.getItem(index);
id = albumItem.albumId;
break
case SortType.ARTISTS:
var artistItem:ArtistItemVO = artistList.getItem(index);
id = artistItem.artistId;
break
}
music.getTrackData(id, tileResultType);
}
protected function dgChangeHandler(event:ListEvent):void
{
var index:Number = event.rowIndex;
var trackItem:TrackItemVO = trackList.getItem(index);
musicPlayerPresenter.playSong(trackItem);
}
private function onArtistsResult(event:ArtistsResultEvent):void
{
music.removeEventListener(ArtistsResultEvent.ARTIST_RESULT_COMPLETED, onArtistsResult);
tileResultType = SortType.ARTISTS;
artistList = new ArtistListVO(event.artistList.list.source);
var item:ArtistItemVO = artistList.getItem(0);
var dp:ArrayCollection = new ArrayCollection();
for (var i:Number = 0; i<artistList.list.length; i++)
{
item = artistList.getItem(i);
dp.addItem({name: item.artistName, count: item.trackCount});
}
musicPlayerMain.tileList.dataProvider = dp;
musicPlayerMain.tileList.selectedIndex = 0;
getTrackList(0);
}
private function onAllAlbumCompleted(event:AlbumDataEvent):void
{
music.removeEventListener(AlbumDataEvent.ALL_ALBUMS_DATA_COMPLETED, onAllAlbumCompleted);
tileResultType = SortType.ALBUMS;
albumList = new AlbumListVO(event.collection.list.source);
var item:AlbumItemVO;
var dp:ArrayCollection = new ArrayCollection();
for (var i:Number = 0; i<albumList.list.length; i++)
{
item = albumList.getItem(i);
dp.addItem({name: item.albumTitle, count: item.trackCount});
}
musicPlayerMain.tileList.dataProvider = dp;
musicPlayerMain.tileList.selectedIndex = 0;
getTrackList(0);
}
private function onAlbumDataComplete(event:AlbumDataEvent):void
{
music.removeEventListener(AlbumDataEvent.ALBUM_DATA_COMPLETED, onAlbumDataComplete);
music.removeEventListener(AlbumDataEvent.ALBUM_DATA_ERROR, onAlbumDataComplete);
var albumList:AlbumListVO = new AlbumListVO(event.collection.list.source);
var albumId:String = albumList.getItem(0).albumId;
music.addEventListener(TrackDataEvent.TRACK_DATA_COMPLETED, onTrackDataComplete);
music.addEventListener(TrackDataEvent.TRACK_DATA_ERROR, function():void { Alert.show("Error getting track data"); });
music.getTrackData(albumId);
}
private function onTrackDataComplete(event:TrackDataEvent):void
{
music.removeEventListener(AlbumDataEvent.ALBUM_DATA_COMPLETED, onAlbumDataComplete);
music.removeEventListener(TrackDataEvent.TRACK_DATA_ERROR, function():void { Alert.show("Error getting track data"); });
trackList = new TrackListVO(event.collection.list.source);
// show track list
musicPlayerMain.dg.dataProvider = trackList.list.source;
// play first song
var trackItem:TrackItemVO = trackList.getItem(0);
musicPlayerPresenter.playSong(trackItem);
// set selected song
musicPlayerMain.dg.selectedIndex = 0;
}
}
}
The creator then takes both the product and the presenter and create a composition:
var musicPlayerView:AbstractMusicPlayerMain = MusicPlayerFactory.createView(type); this.addChild(musicPlayerView); musicPlayerMainPresenter = new MusicPlayerMainPresenter(musicPlayerView);
see complete of the creator:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
backgroundColor="0xe6e6e6"
x="0" y="0"
verticalAlign="middle"
creationComplete="creationCompleteHandler(event)">
<mx:Style source="assets/css/main.css" />
<mx:Script>
<![CDATA[
import com.elad.musicplayer.view.presenter.MusicPlayerMainPresenter;
import com.elad.musicplayer.view.AbstractMusicPlayerMain;
import com.elad.musicplayer.view.LoginForm;
import mx.managers.PopUpManager;
import mx.containers.TitleWindow;
import mx.events.FlexEvent;
import com.elad.musicplayer.view.MusicPlayerFactory;
import com.elad.musicplayer.view.AbstractMusicPlayerMain;
private var loginForm:LoginForm;
private var musicPlayerMainPresenter:MusicPlayerMainPresenter;
// handler after creation complete
protected function creationCompleteHandler(event:FlexEvent):void
{
loginForm = LoginForm(PopUpManager.createPopUp(this, LoginForm, true));
loginForm.addEventListener(LoginForm.LOGIN_SUCCESSFULL, onLogin);
}
// method to load the view
protected function loadView(type:Number):void
{
hBox.visible = false;
hBox = null;
var musicPlayerView:AbstractMusicPlayerMain = MusicPlayerFactory.createView(type);
this.addChild(musicPlayerView);
musicPlayerMainPresenter = new MusicPlayerMainPresenter(musicPlayerView);
}
private function onLogin(event:Event):void
{
hBox.visible = true;
}
]]>
</mx:Script>
<mx:HBox id="hBox" visible="false">
<mx:Button label="320x480" click="loadView(MusicPlayerFactory.MOBILE)" />
<mx:Button label="530x520" click="loadView(MusicPlayerFactory.WEB)" />
</mx:HBox>
</mx:Application>
To view the complete application Click here.
Please note that you have to have an account with MP3tunes Music Locker in order to login into your account and use the application.



















Wouldn’t be better to have AbstractMusicPlayer be an interface instead so that a designer making an MXML view has to use the base components?
You touched a good point.
“AbstractMusicPlayer” is used as the super class and includes members as well as methods. For instance let’s say you want to share the same component between different views, you will ** not ** be able to achieve that with the interface since it’s your contract for the methods not the properties.
By setting members/properties and using them in the presenter you are forcing each view implementation to override the members otherwise you will get an error message.
For instance, if you create a text input component in the Abstract class:
private var text:TextInput;
You purposely didn’t implement it and in case the presenter adds functionality such as an even listener you will get a runtime error since the text class is still set to null.
Abstract classes are not supposed to be instantiate directly and only be extended. When you create Pure AS3 class you can extends the abstract class, but when you create an MXML component based on the abstract class the constructor is being called automatically.
You can implement an interface in an MXML component but, I think it would be nice if Adobe adds a feature to add “extends” to the MXML components.
Should the return type for the “createView” function in Factory class in the third diagram be “AbstractPassiveMainView” instead of “AbstractPassiveView” or is that another class not shown in the diagram?
Yes. Nice catch. I’ll update the diagram soon.
Matthew, here’s a UML diagram of a simple implementation of the pattern:
Any chance to have a look at the whole sources for better understanding of your approach ?
btw, is this somewhat in the vein of PureMVC ?
thanks.
Hi Jeremy,
I have attached a simple example using Adobe AIR. Download it from here:
http://elromdesign.com/blog/Flex/multiview/MultiViewPattern.zip
You are correct it works very well with PureMVC, I have a working project with PureMVC.. The presenter becomes a MediatorPresenter (so you can differentiate that you are using the Multi-View Passive pattern).
Got the source, very helpful, thanks
Jeremy.
Hello Elad. Can you put tests for this project if you saved them?
Thx.
Andrew.
Andrew, do you mean unit testing?
Yes. I try to use TDD after reading your articles, and it will be very interesting to see UnitTests for this project to track your workflow if you saved them of course.
Thx.
Andrew.