Creating mp3 player with RobotLegs (basics tutorial)

Category: home/ActionScript
Date: 2011-02-09
Creating mp3 player with RobotLegs (basics tutorial)

Have you ever wondered how to optimize your ActionScript applications? Have you searched for a nice framework that will save you a lot of time and will organize your structure? If yes then RobotLegs is for you. In this article I'll show you how to use it to create a simple mp3 player which loads a configuration xml file, parse it and play mp3 files.

The source files of the example are available here and the final result here. I used FlashDevelop + Flex SDK, so if you prefer some other tools just get the code from the src folder and include all the .swc files from assets folder. If you want to use FlashDevelop then maybe check this article to find out how to set-up FlashDevelop and Flex SDK.

Shortcuts:
1. What is RobotLegs
2. Main objects
3. Required libraries
4. Initialization of the application
5. The context
6. The startup command
7. The preloading screen
8. The service
9. The model
10. The list
11. The player
12. Overview


1. What is RobotLegs


Robotlegs is a pure AS3 micro-architecture (framework) for developing Flash, Flex, and AIR applications. Robotlegs is narrowly focused on wiring application tiers together and providing a mechanism by which they communicate. Robotlegs seeks to speed up development while providing a time tested architectural solution to common development problems. Robotlegs is not interested in locking you into the framework, your classes are just that, your classes, and should be easily transferable to other frameworks should the need or desire to do so arise in the future.
The framework supplies a default implementation based on the Model-View-Controller meta-design pattern. This implementation provides a strong suggestion as to application structure and design. While it does make your application slightly less portable, it still aims to be as minimally invasive as possible in your concrete classes. By extending the MVCS implementation classes, you are supplied with numerous methods and properties for the sake of convenience.



2. Main objects



A basic diagram of the RobotLegs flow could be found here http://www.robotlegs.org/diagram/.


3. Required libraries


I already included the neccessary libraries in the exemple's archive, but if you want you can download the RobotLegs framework from here http://github.com/robotlegs/robotlegs-framework. I used three additional classes, which are also included in the package.


4. Initialization of the application


As we are using Flex the starting point of our app is Main.mxml.
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" frameRate="31"  backgroundColor="#FFFFFF" width="600" height="300" layout="absolute" applicationComplete="initApp()" backgroundGradientColors="[0xFFFFFF, 0xFFFFFF]">
	<mx:Script>
		<![CDATA[
			
		import lib.App;
		import com.krasimirtsonev.utils.UIComponentWrapper;
		
		private function initApp():void {
			addChild(new App());
		}
			
		]]>
	</mx:Script>	
</mx:Application>
Just two things - setting initApp as a value of applicationComplete and adding an instance of class App on the stage.
The App class is also very simple. I just created a holder (MovieClip) and added it. I prefer to create a main holder clip, instead of attaching all the stuff directly to the main component. As you probably know, when we work with Flex we can't just attach MovieClip to Canvas object. So I created a function that wraps the clip in UIComponent. The context of the player is created at line 21.
package lib {
	
	import flash.display.MovieClip;
	import flash.events.Event;
	import lib.contexts.PlayerContext;
	import mx.containers.Canvas;
	import com.krasimirtsonev.utils.UIComponentWrapper;
	
	public class App extends Canvas {
		
		private var _holder:MovieClip;
		private var _context:PlayerContext;
				
		public function App() {
			
			// adding the main holder
			_holder = new MovieClip();
			addChild(UIComponentWrapper.wrap(_holder));
			
			// creating the Context of the application
			_context = new PlayerContext(_holder);
			
		}
	}
	
}



5. The Context


package lib.contexts {
	
	import com.krasimirtsonev.utils.Delegate;
	import flash.display.DisplayObjectContainer;
	import lib.models.TracksModel;
	import lib.services.remote.DataLoaderService;
	import lib.controllers.commands.StartupCommand;
	import lib.ui.list.ListMediator;
	import lib.ui.list.ListView;
	import lib.ui.player.PlayerMediator;
	import lib.ui.player.PlayerView;
	import org.robotlegs.mvcs.Context;
	import org.robotlegs.base.ContextEvent;
	import com.krasimirtsonev.utils.Debug;
	import lib.ui.preloader.PreloaderMediator;
	import lib.ui.preloader.PreloaderView;
	import flash.utils.setTimeout;
	public class PlayerContext extends Context  {
		
		public function PlayerContext(view:DisplayObjectContainer) {
			super(view);
		}
		private function get className():String {
			return "PageComposerContext";
		}
		override public function startup():void {
			Debug.echo(className + " startup");
			
			// map singletons
			injector.mapSingleton(DataLoaderService);
			injector.mapSingleton(TracksModel);
			
			// mapping the commands
			commandMap.mapEvent(ContextEvent.STARTUP, StartupCommand, ContextEvent);
			
			// mapping views to their mediators
			mediatorMap.mapView(PreloaderView, PreloaderMediator);
			mediatorMap.mapView(ListView, ListMediator);
			mediatorMap.mapView(PlayerView, PlayerMediator);
			
			// adding the views to the stage
			contextView.addChild(new PreloaderView());
			contextView.addChild(new ListView());
			contextView.addChild(new PlayerView());
			
			// calling the startup command			
			dispatchEvent(new ContextEvent(ContextEvent.STARTUP));			
			
		}	
	}
}
As a parameter of the Context's constructor you should pass a DisplayObjectContainer, which is available later as a contextView property. In this case I passed the _holder clip. Generaly RobotLegs listens for ADDED_TO_STAGE event of that DisplayObjectContainer to call the startup method. So you should not add any logic in the constructor of the Context, you should do that in the startup function. And make sure that you add your main holder to the stage. Either way the startup method will not be fired.
The first thing that I did was to map the service and the model as singleton by using the injector. By doing that I'm sure that wherever I need some of them the framework will return only one and the same instance. For example, getting an access to the model:
// in your context
injector.mapSingleton(MyCustomModel);
// in your custom class
public class Test extends Actor {
	
	[Inject]
	public var model:MyCustomModel;
	
	public function Test() {
		model.myCustomMethod();
	}
	
}
Have in mind that if you want to use the Inject tag your class should extends at least Actor. And also your property should be defined as public. Either way RL will not have an access to it.
On line 35 I used the commandMap to map the ContextEvent.STARTUP to a command, which means that if we (or the framework) dispatch ContextEvent.STARTUP the execute method of StartupCommand will be called.
Lines 38, 39 and 40 - mapping the mediators to their views.
Lines 43, 44, and 45 - adding the views to the stages. Every mediator has a method called onRegister, which normaly is fired when the ADDED_TO_STAGE event of the view is dispatched. So basically if you have any logic in that method it will not be executed till the view is added to the stage. That's why I added the views right now, in the beginning of the application's flow. It is not very good practice, because if you have a lot of views (UI components) you can't add them all at the same time. And also in most cases the logic needs to show some things after some actions not at the startup. But for this simple example it's fine to leave it like that. And remember that you have an access to the contextView in all your commands, and mediators so you could add whatever you want when you need.
At line 48 I dispatched the ContextEvent.STARTUP event, which fires the execution of StartupCommand.


6. The startup command


package lib.controllers.commands {
	
	import flash.events.Event;
	import lib.controllers.events.DataEvent;
	import lib.models.TracksModel;
	import lib.services.remote.DataLoaderService;
	import lib.controllers.events.PreloaderEvent;
	import org.robotlegs.mvcs.Command;
	import com.krasimirtsonev.utils.Debug;
	public class StartupCommand extends Command {
		
		private var _xmlURL:String = "tracks.xml";
		
		[Inject]
		public var dataLoaderService:DataLoaderService;
		
		[Inject]
		public var model:TracksModel;
		
		private function get className():String {
			return "StartupCommand";
		}
		override public function execute():void {
			Debug.echo(className + " execute");
			
			// adding listeners for the DataLoaderService's events
			eventDispatcher.addEventListener(DataEvent.ON_DATA_FAILED_TO_LOAD, onDataFailedToLoad);
			eventDispatcher.addEventListener(DataEvent.ON_DATA_LOADED, onDataLoaded);
			
			// firing the service
			dataLoaderService.load(_xmlURL);
			
			// setting the preloader text
			dispatch(new PreloaderEvent(PreloaderEvent.SET_TEXT, "Loading xml ..."));
			
		}
		private function onDataLoaded(e:DataEvent):void {
			dispatch(new PreloaderEvent(PreloaderEvent.SET_VISIBLE, false));
			model.xml = e.data;
		}
		private function onDataFailedToLoad(e:DataEvent):void {
			dispatch(new PreloaderEvent(PreloaderEvent.SET_TEXT, "Failed to load the xml data (" + _xmlURL + "). " + e.data));
		}
	}
}
In my opinion the purpose of the startup command is to trigger the beginning operations of the application. In our case the first thing that we should do is to load the xml file, which contains the description of the songs. Before to continuing we should mention the eventDispatcher. One of the most important features of the RL is the eventDispatcher. All the actors of the framework have access to this dispatcher, which means that you can dispatch an event almost from everywhere and of course to catch it wherever you want. As you can see I added two listeners for the DataEvent events. I strongly recommend the usage of custom events, because you can pass to them what you need and there is no chance to make a typo mistake. Be sure that you override the clone method as well. It's really important, because in some cases you will need to redispatch. The source code of the DataEvent class is available here.


7. The preloading screen


The PreloaderView class is responsible for the preloading screen. It has a mediator PreloaderMediator mapped to it. So let's check the code of both classes:
// the view
package lib.ui.preloader {
	
	import flash.display.MovieClip;
	public class PreloaderView extends MovieClip {
		
		private var _clip:MovieClip;
		
		public function PreloaderView() {
			_clip = new A_FieldClip();
			_clip.field.text = "";
			addChild(_clip);
			x = y = 15;
			visible = false;
		}
		public function setText(str:String):void {
			_clip.field.htmlText = str;
			visible = true;
		}
	}
}
// the mediator
package lib.ui.preloader {
	
	import lib.controllers.events.PreloaderEvent;
	import org.robotlegs.mvcs.Mediator;
	import com.krasimirtsonev.utils.Debug;
	public class PreloaderMediator extends Mediator {
		
		[Inject]
		public var view:PreloaderView;
		
		private function get className():String {
			return "PreloaderMediator";
		}
		override public function onRegister():void {
			Debug.echo(className + " onRegister");
			
			eventMap.mapListener(eventDispatcher, PreloaderEvent.SET_TEXT, onSetText, PreloaderEvent);
			eventMap.mapListener(eventDispatcher, PreloaderEvent.SET_VISIBLE, onSetVisible, PreloaderEvent);
			
		}
		private function onSetText(e:PreloaderEvent):void {
			view.setText(e.data);
		}
		private function onSetVisible(e:PreloaderEvent):void {
			view.visible = e.data;
		}
	}
}
The view has only one public method which sets the text on the screen. The mediator adds two listeners for a custom event PreloaderEvent. The first one is for text changing, and the second one is for managing the visibility of the preloader screen. As you can see we can control everything only by dispatching events to the RL's eventDispatcher. We map the mediator's methods and they will be called when we dispatch PreloaderEvent.SET_TEXT and PreloaderEvent.SET_VISIBLE. If we check the code of the startup command:
// changing the text
dispatch(new PreloaderEvent(PreloaderEvent.SET_TEXT, "Loading xml ..."));
...
// hiding the preloader screen
dispatch(new PreloaderEvent(PreloaderEvent.SET_VISIBLE, false));
Please note that we have two different operations and I passed two different types of data (String and Boolean) as a payload. Here is the code of the PreloaderEvent class.
package lib.controllers.events {
	
	import flash.events.Event;
	public class PreloaderEvent extends Event {
		
		public static const SET_TEXT:String = "PreloaderEventSetText";
		public static const SET_VISIBLE:String = "PreloaderEventSetVisible";
		
		private var _data:*;
		
		public function PreloaderEvent(type:String, data:*) {
			super(type);
			_data = data;
		}
		override public function clone():Event {
			return new PreloaderEvent(type, _data);
		}
		public function get data():* {
			return _data;
		}	
	}
}



8. The service


package lib.services.remote {
	
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import org.robotlegs.mvcs.Actor;
	import com.krasimirtsonev.utils.Debug;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import lib.controllers.events.DataEvent;
	public class DataLoaderService extends Actor {
		
		private var _url:String = "";
		private var _loader:URLLoader;
		
		public function DataLoaderService() {
			super();
			_loader = new URLLoader();
			_loader.addEventListener(Event.COMPLETE, onDataLoad);
			_loader.addEventListener(IOErrorEvent.IO_ERROR, onDataFiledToLoad);
			_loader.addEventListener(IOErrorEvent.NETWORK_ERROR, onDataFiledToLoad);
			_loader.addEventListener(IOErrorEvent.VERIFY_ERROR, onDataFiledToLoad);
			_loader.addEventListener(IOErrorEvent.DISK_ERROR, onDataFiledToLoad);
		}
		private function get className():String {
			return "XMLLoader";
		}
		public function load(url:String):void {
			Debug.echo(className + " loadXML url=" + url);
			_url = url;
			_loader.load(new URLRequest(_url));			
		}
		private function onDataLoad(e:Event):void {
			eventDispatcher.dispatchEvent(new DataEvent(DataEvent.ON_DATA_LOADED, e.target.data));
		}
		private function onDataFiledToLoad(e:IOErrorEvent):void {
			eventDispatcher.dispatchEvent(new DataEvent(DataEvent.ON_DATA_FAILED_TO_LOAD, e.text));
		}
	}
}

The service class extends Actor, because I wanted to have access to the eventDispatcher. It can notify the framework for the loaded data or for errors. After the data is successfully loaded and the DataEvent.ON_DATA_LOADED event is dispatched the startup command sends the xml string to the model:
private function onDataLoaded(e:DataEvent):void {
	dispatch(new PreloaderEvent(PreloaderEvent.SET_VISIBLE, false));
	model.xml = e.data;
}



9. The model


package lib.models {	
	
	import flash.events.Event;
	import lib.controllers.events.DataEvent;
	import lib.models.vos.TrackVO;
	import org.robotlegs.mvcs.Actor;
	import com.krasimirtsonev.utils.Debug;
	public class TracksModel extends Actor {
		
		private var _tracks:Array;
		
		public function TracksModel() {
			super();
		}
		private function get className():String {
			return "TracksModel";
		}
		public function set xml(xmlStr:String):void {
			
			_tracks = [];
			
			var xml:XML = new XML(xmlStr);
			var numOfTrags:int = xml.tracks.t.length();
			for(var i:int=0; i<numOfTrags; i++) {
				var trackNode:XML = xml.tracks.t[i];
				var vo:TrackVO = new TrackVO();
				vo.name = trackNode.@name;
				vo.file = trackNode.@file;
				_tracks.push(vo);
			}
			
			dispatch(new DataEvent(DataEvent.ON_DATA_UPDATED, _tracks));
			
		}
		public function get tracks():Array {
			return _tracks;
		}
	}
}
The model parses the xml, creates VO objects and dispatches a new DataEvent event. So in this moment we don't have anything visible on the stage and we have a filled model with data which is waiting to be used. Have in mind that the model has getter tracks which provide an access to the VOs' array.


10. The list


package lib.ui.list {
	
	import fl.data.DataProvider;
	import flash.display.MovieClip;
	import com.krasimirtsonev.utils.Debug;
	import flash.events.Event;
	public class ListView extends MovieClip {
		
		private var _clip:MovieClip;
		
		public function ListView() {
			_clip = new A_ListClip();
			_clip.list.width = _clip.list.height = 300;
			_clip.list.addEventListener(Event.CHANGE, onListItemSelected);
			addChild(_clip);
			visible = false;
		}
		private function get className():String {
			return "ListView";
		}
		public function set dataProvider(dp:Array):void {
			_clip.list.dataProvider = new DataProvider(dp);
			visible = true;
		}
		public function get selectedItem():Object {
			return _clip.list.selectedItem;
		}
		private function onListItemSelected(e:Event):void {
			dispatchEvent(new Event(Event.CHANGE));
		}	
	}
}
The ListView class is a MovieClip which contains a list component. We can do three things with it.


Of course the ListMediator class is responsible for all this.
package lib.ui.list {
	
	import flash.events.Event;
	import lib.controllers.events.DataEvent;
	import lib.controllers.events.ListEvent;
	import lib.models.vos.TrackVO;
	import org.robotlegs.mvcs.Mediator;
	import com.krasimirtsonev.utils.Debug;
	public class ListMediator extends Mediator {
		
		[Inject]
		public var view:ListView;
		
		private function get className():String {
			return "ListMediator";
		}
		override public function onRegister():void {
			Debug.echo(className + " onRegister");
			eventMap.mapListener(eventDispatcher, DataEvent.ON_DATA_UPDATED, onDataUpdated, DataEvent);			
			eventMap.mapListener(view, Event.CHANGE, onListItemSelected, Event);			
		}
		private function onDataUpdated(e:DataEvent):void {
			var dataProvider:Array = [];
			var numOfTracks:int = e.data.length;
			for(var i:int = 0; i < numOfTracks; i++) {
				var trackVO:TrackVO = e.data[i] as TrackVO;
				dataProvider.push({label:(i+1) + ". " + trackVO.name, data:trackVO});
			}
			view.dataProvider = dataProvider;
		}
		private function onListItemSelected(e:Event):void {
			dispatch(new ListEvent(ListEvent.ON_LIST_ITEM_SELECTED, view.selectedItem.data));
		}
	}
}
This mediator listens for two events:



11. The player


Because the idea of this article is to show you how to use RobotLegs I'll not focus on the PlayerView class. Generally it has a public method called playFile, which accepts two parameters - a mp3 file for playing and the title of the song. You can check the source code here.
Here is the mediator:
package lib.ui.player {
	
	import lib.controllers.events.ListEvent;
	import org.robotlegs.mvcs.Mediator;
	import com.krasimirtsonev.utils.Debug;
	public class PlayerMediator extends Mediator {
		
		[Inject]
		public var view:PlayerView;
		
		private function get className():String {
			return "PlayerMediator";
		}
		override public function onRegister():void {
			Debug.echo(className + " onRegister");
			
			eventMap.mapListener(eventDispatcher, ListEvent.ON_LIST_ITEM_SELECTED, onListItemSelected, ListEvent);
			
		}
		private function onListItemSelected(e:ListEvent):void {
			view.playFile(e.trackVO.file, e.trackVO.name);
		}
	}
}
The job of this mediator is to listen for change in the list component. If there is an item selected then it calls the playFile of the PlayerView class.


12. Overview




P.S.
this article doesn't pretend to show you the best practices in RobotLegs. It's just my own interpretation of the framework. So if you have any ideas how to improve the code please post a comment below.





blog comments powered by Disqus