Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

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

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.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn.