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.
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
- 2.1. Context - the context is a place where you have to do the main initialization of the framework elements. You could think about it as a starting point of your application/module. In one project you could have many contexts, but you should manage the relation between them. Some of the properties of the Context object that you should know:
- 2.2.1. injector - by accessing this property you are using the dependency injection mechanism. Generally speaking that means that you can "map" some of your custom classes and use them somewhere else in the framework with no need to create new objects. RobotLegs uses metadata tags to realize this mechanism.
- 2.2.2. mediatorMap - the mediator map connects your views to their mediators
- 2.2.3. commandMap - the command map links specific event to specific command
- 2.2. Views - the view contains the user interface elements and their logic. The objects of this type are responsible for collecting the user's input. For example the list control of the mp3 player is part of a view class. It's important to keep the view as independent as possible, i.e. to communicate only with its API. Also the views should not have references to any other objects of the application.
- 2.3. Mediators - these objects are tightly connected to the views. The mediator is responsible to update the view via its API and to catch events that the view dispatches. In other words the mediators connect the view to the framework.
- 2.4. Commands - instead of creating functions-listeners for every event (because RobotLegs uses a lot of events) that occurs you could map/connect specific event to a command. And when the event is dispatched the command will be executed.
- 2.5. Services - the services are classes that help you to get data from external resources. They don't hold the data, just send and receive. For example in this project I created a service that loads the xml configuration file and dispatches an event notifing the framework that the data is receved.
- 2.6. Models - the models hold the data of the application. They also operate with it and provide API methods for access.
- 2.7. VOs - the value objects are small classes that also contain the data but they don't operate with it. As you will see below, the mp3 player's xml data is converted to an array (which is held by the model) of value objects (which contain the file name and the name of the song).
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.
<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 & lt; 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.
- - set the content (set the dataProvider)
- - get the selected item
- - listen for event when the user clicks on some of the list's items
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:
- 1. DataEvent.ON_DATA_UPDATED - it's dispatched by the model when the data is parsed. The method onDataUpdated creates an array which is set as a dataProvider of the list component.
- 2. Event.CHANGE - the dispatcher of this event is the view. What I did in the listener is to dispatch a new event to the framework, because Event.CHANGE is only available for the mediator. The new event contains a VO as a payload.
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
- 12.1. The application's flow
- 12.1.1. - Creating Context
- 12.1.2. - Showing the preloader screen
- 12.1.3. - Forcing the Service to load the data
- 12.1.4. - Passing the data to the model
- 12.1.5. - Parsing the data and converting it to VOs by the model
- 12.1.6. - Filling the list component
- 12.1.7. - Playing the mp3 file when some of the list items is selected
- 12.2. The application's file structure [1]
- 12.3. Helpful links
- 12.3.1. RobotLegs official page
- 12.3.2. RobotLegs on GITHUB
- 12.3.3. RobotLegs comunity
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.