An Introduction to Robotlegs AS3 Part 3: Services

This is the third part in the introductory series covering Robotlegs AS3 here on InsideRIA. In the first part in the series we learned what Robotlegs AS3 is and had a brief “HelloWorld” introduction to the Robotlegs Context and Mediator classes. In the second part we explored Models. This article is going to expand on those concepts and introduce Services.

robotlegs.jpg

If you missed the first two parts I recommend looking at them first:

What is a Service?

The name of this website is InsideRIA. A Service is your application’s link to the I in RIA. Services provide your application an API to access web services, data sources, and other applications. This could be accessing the Twitter API, your company/client’s web server, the file system, or a host of other sources of data outside of your application.

A Service makes remote calls and parses the results. After the results are parsed, the service will notify the rest of the application that data is ready and/or update a Model with the results. We want external data to live as short of a life as possible within our applications. As soon as we get the results, they are converted to a format that is specific to the application domain. This might be XML, JSON, or typed Objects from an AMF source. When we are working in a “controlled” environment, meaning the server-side mid-tier is within our control, sometimes we can avoid parsing altogether because the returned objects are already part of the application domain. In many situations we are accessing third-party servers and receiving “foreign” data. This is what we want to parse.

Let’s make something!

So with the definition behind us, lets look at how a Service functions within a Robotlegs AS3 application. This example is going to make use of the basic Twitter Search API.

serve-talks-to-twitter.png

The diagram above is the general overview of how our service will function. It receives a request, asks the server for some data, parses the results, and notifies the rest of the application. The requests are generally made from Mediators and Commands. In this example, we are going to keep it simple and make requests to the service from a Mediator. In the next article we will talk about how Commands are a great place to access services. Let’s take a look at the finished widget and then build it from the ground up.

Download ServiceExample.zip

So it’s not the next TweetDeck, but it is about as simple as it gets when accessing the Twitter API. The widget access the Twitter Search API and returns the current results for “robotlegs.” We’ll start simple, and add complexity later if we need to. Let’s take a look at the pieces that go into to putting this together:

ServiceExample.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<s:Application xmlns:fx="https://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:service="robotlegs.examples.service.*" 
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:view="robotlegs.examples.service.view.*"
               width="300"
               height="300">
    <fx:Declarations>
        <service:ServiceExampleContext contextView="{this}"/>
    </fx:Declarations>
    <view:SearchResultsView/>
</s:Application>

This is the main application view. You will notice that I have already made an instance of the ServiceExampleContext available in the Declarations tag. If you haven’t had a chance to read parts 1 and 2 in the series, I recommend doing so now. They cover basics of creating and initializing the Context. In addition to the ServiceExampleContext, you see that the SearchResultView has also been added to the application. This example only has the one view. Take a look at the context:

ServiceExampleContext.as

1
2
3
4
5
6
7
public class ServiceExampleContext extends Context
{
    override public function startup():void
    {
        mediatorMap.mapView(SearchResultsView, SearchResultsViewMediator);
    }
}

In the SearchExampleContext we are mapping a mediator for the SearchResultsView. The SearchResultsView is a sub-class of the Spark List component:

SearchResultsView.mxml

1
2
3
4
5
6
7
8
9
<?xml version="1.0"?>
<s:List
        xmlns:fx="https://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        itemRenderer="robotlegs.examples.service.view.renderers.SearchResultItemRenderer"
        height="100%" width="300">
    <s:layout><s:VerticalLayout gap="0" paddingBottom="10" paddingRight="10" paddingLeft="10" paddingTop="10"/></s:layout>
</s:List>

The SearchResultsView needs a mediator to allow it to communicate with the rest of the application:

SearchResultsViewMediator.as

1
2
3
4
5
6
7
8
9
10
public class SearchResultsViewMediator extends Mediator
{
    [Inject]
    public var view:SearchResultsView;
 
    override public function onRegister():void
    {
        //Make the magic!
    }
}

Now that the SearchResultsView is properly mediated, we are ready to start looking at the service that will connect to the remote Twitter server and return some data for us to list. If you were looking closely at the SearchResultsView you will see that we have also given it an ItemRenderer. We will be looking at that in just a bit. First, let’s take a look at the basic service class that will connect to Twitter and fetch our data:

TwitterSearchService.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class TwitterSearchService extends Actor implements ISearchService
{
    private var loader:URLLoader;
 
    public function TwitterSearchService()
    {
        loader = new URLLoader();
    }
 
    private const TWITTER_SEARCH_ENDPOINT:String = "https://search.twitter.com/search.json";
    public function getResults(forQuery:String = "robotlegs"):void
    {
        var urlRequest:URLRequest = new URLRequest(TWITTER_SEARCH_ENDPOINT);
        var params:URLVariables = new URLVariables();
 
        params.q = HTMLStringTools.htmlEscape(forQuery);
        params.rpp = 100;
        urlRequest.data = params;
 
        addLoaderListeners();
 
        loader.load(urlRequest);
    }
 
    private function handleError(event:SecurityErrorEvent):void
    {
        removeLoaderListeners();
    }
 
    private function handleLoadComplete(event:Event):void
    {
        removeLoaderListeners();
    }
 
    private function addLoaderListeners():void
    {
        loader.addEventListener(IOErrorEvent.IO_ERROR, handleError);
        loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleError);
        loader.addEventListener(Event.COMPLETE, handleLoadComplete);
    }
 
    private function removeLoaderListeners():void
    {
        loader.removeEventListener(IOErrorEvent.IO_ERROR, handleError);
        loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleError);
        loader.removeEventListener(Event.COMPLETE, handleLoadComplete);
    }
}

TwitterSearchService has one public method, getResults(). It fetches results for a search string from the Twitter Search API. It is using the URLLoader to accomplish this and accessing the API directly. There are a number of excellent Twitter API libraries for Actionscript available. I’d highly recommend investigating them if you are trying to write the next TweetDeck. With the URLLoader the TwitterSearchService makes a simple request to the appropriate endpoint. Listeners are added to the URLLoader, and the fault/result handlers catch whatever is thrown back. Right now handleLoadComplete isn’t actually doing anything except removing the listeners, which isn’t very useful, but we will remedy that in just a moment.

Note that TwitterSearchService extends Actor and implements ISearchService. Actor simply provides convenient access to the EventDispatcher for this context. With services, it is generally considered best practice to implement an interface. Take a look at ISearchService:

ISearchService.as

1
2
3
4
public interface ISearchService
{
    function getResults(forQuery:String="robotlegs"):void;
}

When you implement an interface for a service class, it makes it really easy to swap the service out. Why would you want to do that? When connecting to remote services it is sometimes more effectiveduring development to create mock data services with 0 latency. You could make a MockTwitterJsonService that returned the same static, hardcoded results every time. This approach is critical for unit testing, which will be covered in another article, as well as just general development. Many times you are will find yourself developing in parallel to the team developing the service. The ability to create and replace mock services with live ones provides a lot of flexibility.

You will recall in the definition of a Robotlegs service above that we talked about parsing data as soon as it is returned from the remote service. In the case of the Twitter Search API endpoint we are using, we receive a JSON string as the result. JSON is a great format, but we don’t want to start passing around raw JSON strings in our application. It loses the benefit of strong typing, and besides, we want to pick and choose what information we are going to use in our application. In this case, we only need the user name and actual tweet text.

Tweet.as

1
2
3
4
5
public class Tweet
{
    public var user:String;
    public var message:String;
}

This class needs a sandwich! Not a lot going on here, but we now have something to convert the results to.

TwitterSearchResultParser.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TwitterSearchResultParser implements ISearchResultParser
{
    public function parseSearchResults(results:Object):Array
    {
        var decoder:JSONDecoder = new JSONDecoder(String(results));
        var resultObjects:Array = decoder.getValue().results;
        var tweets:Array = [];
        for each(var result:Object in resultObjects)
        {
            var tweet:Tweet = new Tweet();
            tweet.user = result.from_user;
            tweet.message = HTMLStringTools.htmlUnescape(result.text);
 
            tweets.push(tweet)
        }
        return tweets;
    }
}

The results parser is a straight forward utility class. For the same reasons the service implements an interface, the results parser does as well. It has one method, parseSearchResults() which takes a results argument. The results from Twitter will be a string, but other results might be XML if we were to use local mock data or change which Twitter service we were accessing. The JSON string is encoded into objects, which are then converted to our Tweet value object from above. The resulting Array is returned as the result. Now that the parser is ready to go, we want to supply it to the service so that it can parse the results.

You have several options for giving the service access to the parser class. You could make its methods static, you could create a new instance of the parser class within the service, or you can leverage dependency injection to supply the parser to the service class. The problem with the first two options is that it makes it a lot harder to swap out the parser class later if we needed to.

We are in the land of dependency injection now, so why not map the parser for injection? If we do this, it will be one single line in our context to change if we want to switch it out. There is no digging around in our classes commenting out lines and changing types. because both the parser and service conform to interfaces. If you’d like to see how easy it is to swap out a service, take a look at the Flickr Image Gallery in the Robotlegs Demo Bundle.

Let’s look at mapping the service and parser interfaces in the context:

ServiceExampleContext.as

1
2
3
4
5
6
7
override public function startup():void
{
    mediatorMap.mapView(SearchResultsView, SearchResultsViewMediator);
 
    injector.mapSingletonOf(ISearchService, TwitterSearchService);
    injector.mapSingletonOf(ISearchResultParser, TwitterSearchResultParser);
}

This is the first time we have used the mapSingletonOf() method of the injector. It behaves much the same way as mapSingleton, with one critical difference. With mapSingletonOf() you are mapping the class you wish to inject via its interface or base class. Let’s take a look at the TwitterSearchService and see how this is injected into our classes:

TwitterSearchService.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private var _parser:ISearchResultParser;
 
[Inject]
public function set parser(value:ISearchResultParser):void
{
    _parser = value;
}
 
private var loader:URLLoader;
 
public function TwitterSearchService()
{
    loader = new URLLoader();
}

With the above code added to the TwitterSearchService you are now injecting the parser that was mapped into it. There are a couple of interesting things happening here. You will notice right away that the parser is of type ISearchResultParser. Since we used mapSingletonOf() to map our parser, it looks for a property of the base class or interface type that we mapped. It will actually inject the TwitterSearchResultParser, but we will access it via its interface. This approach is referred to as polymorphism and is a power object-oriented concept.

We are also placing the [Inject] tag above the setter function and not the property itself. In addition to property injection, which we have used up until now, Robotlegs supports setter and constructor injection. In this case we are using setter injection to supply the value to the parser property’s setter function. Constructor injection doesn’t even require the [Inject] metadata to function. We will talk about construction injection later, for now we need to remember to add the setter function to the ISearchService interface:

ISearchService.as

1
2
3
4
5
6
public interface ISearchService
{
    function getResults(forQuery:String="robotlegs"):void;
 
    function set parser(value:ISearchResultParser):void;
}

Now that the parser is all sorted out, we are ready to actually use it in the service. The result handler is the likely spot to make use of the parser, so we will add some code there:

TwitterSearchService.as

1
2
3
4
5
6
7
private function handleLoadComplete(event:Event):void
{
    var data:Object = loader.data;
    var results:Array = _parser.parseSearchResults(data);
    dispatch(new SearchResultEvent(SearchResultEvent.RECEIVED, results))
    removeLoaderListeners();
}

There we go, the data is extracted from the URLLoader, sent to the parser to be converted into an Array of Tweet objects, an event is dispatched notifying the rest of the app that we now have results, and the event listeners are removed. The SearchResultEvent is a simple custom event that has an array as its payload:

SearchResultEvent.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SearchResultEvent extends Event
{
    public static const RECEIVED:String = "searchResultsReceived";
 
    private var _results:Array;
    public function get results():Array
    {
        return _results;
    }
 
    public function SearchResultEvent(type:String, results:Array = null)
    {
        super(type);
        _results = results;
    }
 
    override public function clone():Event
    {
        return new SearchResultEvent(type, results)
    }
}

At this point in development we have a service that should work, a mediated view component, and an event being dispatched to deaf ears. It would be more accurate to say an event that is never dispatched at all. We haven’t wired the service anywhere to call getResult(). We will take care of that in the SearchResultViewMediator:

SearchResultsViewMediator.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SearchResultsViewMediator extends Mediator
{
    [Inject]
    public var view:SearchResultsView;
 
    [Inject]
    public var service:ISearchService;
 
    override public function onRegister():void
    {
        service.getResults();
 
        addContextListener(SearchResultEvent.RECEIVED, handleResultsReceived)
    }
 
    private function handleResultsReceived(event:SearchResultEvent):void
    {
        view.dataProvider = new ArrayCollection(event.results);
    }
}

Now we are talking! Again the service is injected via its interface, and in the onRegister() we make a call to getResults(). The magical hamsters at Twitter HQ generate some content that our service parses and notifies the rest of the application with an event carrying the data. The mediator is listening for that event, and in handleResultsReceived it supplies the results to the view’s dataProvider as an ArrayCollection. All we need to add is the ItemRenderer and we will have a fully semi-functional Twitter client:

SearchResultItemRenderer.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0"?>
<s:ItemRenderer
        xmlns:fx="https://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        width="260" height="85">
    <fx:Script><![CDATA[
        import robotlegs.examples.service.model.Tweet;
 
        override public function set data(value:Object):void
        {
            super.data = value;
            var searchResult:Tweet = value as Tweet;
            if(searchResult)
            {
                message.text = searchResult.message;
                username.text = searchResult.user;
            }
            else
            {
                message.text = "";
                username.text = "";
            }
        }
 ]]></fx:Script>
    <s:layout>
        <s:VerticalLayout paddingBottom="5" paddingRight="5" paddingLeft="5" paddingTop="5"/>
    </s:layout>
    <s:Label id="username" width="100%" maxDisplayedLines="1" fontWeight="bold"/>
    <s:Label id="message" width="100%" maxDisplayedLines="4" />
</s:ItemRenderer>

And there you have it. A list of tweets that mention Robotlegs. Glorious indeed! I would encourage you to play with this application a bit. Add a control bar to change the search terms. Add the ability to login, retrieve your timeline, and maybe post a Tweet. If you don’t want to piss off all your nerd friends, start a new Twitter account to play with while you develop. I hear they have plenty of extras available.

Now that we have services, we’ve almost covered the full scope of tools for utilizing the Robotlegs MVC+S implementation. All that we have left to cover is Commands. Commands will let us decouple the code and provide re-usable “actions” that are triggered by events.

If you can’t wait, there are articles on my blog (and lots of others across the internets) dealing with various Robotlegs topics including this 25 minute screencast. John Lindquist has a Hello World screencast on his blog. Additionally there is a Best Practices document that has proven helpful for many. You can always hit the Robotlegs Knowledge Base for help and support. It has an active group of community volunteers, including myself, that diligently answer questions regarding all things Robotlegs. Additionally I participated in writing Flex 4 in Action, which has 22 pages of Robotlegs material.

Want to make more money and love what you do? If you answered "Yes!" then you will enjoy my Consultancy Masterclass Sketchnotes.

Brennan charges $1,799 for his masterclass and it is worth every penny. If you are on the fence about the class, or would like to look at the "essence" of the class in 10 pages of lovingly illustrated sketchnotes, click below and they are yours for $9.99 suggested minimum price. It is a steal when you consider how these concepts can boost your revenues as a freelancer or consultant!

"Awesome! I love these." - Brennan Dunn

Get the DRM Free PDF Now!

100% satisfaction gurantee. If you aren't happy with these notes, for any reason, just let me know and I will refund your money!